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-export
> 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,80 @@
/*
* 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 {
getFileExtension,
getInitialPluginMetaInfo,
isValidURL,
} from '../src/component/file-import/utils';
vi.mock('@coze-arch/logger', () => ({
logger: {
info: vi.fn(),
persist: {
error: vi.fn(),
},
},
}));
vi.mock('@coze-arch/bot-error', () => ({
CustomError: vi.fn(),
}));
vi.mock('@coze-arch/bot-utils', () => ({
safeJSONParse: JSON.parse,
}));
describe('getFileExtension', () => {
it('yaml file extension', () => {
const res = getFileExtension('test.yaml');
expect(res).toEqual('yaml');
});
it('json file extension', () => {
const res = getFileExtension('test.json');
expect(res).toEqual('json');
});
});
describe('isValidURL', () => {
it('is not valid url', () => {
const res = isValidURL('app//ddd');
expect(res).toEqual(false);
});
it('is valid url', () => {
const res = isValidURL('https://www.coze.com/hello');
expect(res).toEqual(true);
});
});
describe('getInitialPluginMetaInfo', () => {
it('get initial info', () => {
const data: any = {
aiPlugin: {
name_for_human: '1',
description_for_human: '1',
auth: { type: 'none' },
},
openAPI: { servers: [{ url: 'url' }] },
};
const res = getInitialPluginMetaInfo(data);
expect(res.name).toEqual('1');
expect(res.desc).toEqual('1');
expect(res.auth_type?.[0]).toEqual(0);
expect(res.url).toEqual('url');
});
});

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,133 @@
{
"name": "@coze-agent-ide/bot-plugin-export",
"version": "0.0.1",
"description": "plugin 导出模块",
"license": "Apache-2.0",
"author": "lihuiwen.123@bytedance.com",
"maintainers": [],
"exports": {
".": "./src/index.tsx",
"./agentSkillPluginModal/hooks": "./src/component/agent-skill-plugin-modal/hooks.tsx",
"./asyncSetting": "./src/component/async-setting/index.tsx",
"./pluginFeatModal": "./src/component/plugin-feat-modal/index.tsx",
"./pluginFeatModal/featButton": "./src/component/plugin-feat-modal/feat-button/index.tsx",
"./botEdit": "./src/component/bot_edit/index.ts",
"./fileImport": "./src/component/file-import/index.tsx",
"./editor": "./src/component/editor/index.ts",
"./pluginDocs": "./src/component/plugin-docs/index.tsx"
},
"main": "src/index.tsx",
"typesVersions": {
"*": {
"agentSkillPluginModal/hooks": [
"./src/component/agent-skill-plugin-modal/hooks.tsx"
],
"asyncSetting": [
"./src/component/async-setting/index.tsx"
],
"pluginFeatModal": [
"./src/component/plugin-feat-modal/index.tsx"
],
"pluginFeatModal/featButton": [
"./src/component/plugin-feat-modal/feat-button/index.tsx"
],
"botEdit": [
"./src/component/bot_edit/index.ts"
],
"fileImport": [
"./src/component/file-import/index.tsx"
],
"editor": [
"./src/component/editor/index.ts"
],
"pluginDocs": [
"./src/component/plugin-docs/index.tsx"
]
}
},
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@blueprintjs/core": "^5.1.5",
"@coze-agent-ide/bot-plugin-mock-set": "workspace:*",
"@coze-agent-ide/bot-plugin-tools": "workspace:*",
"@coze-agent-ide/plugin-modal-adapter": "workspace:*",
"@coze-agent-ide/plugin-shared": "workspace:*",
"@coze-agent-ide/tool": "workspace:*",
"@coze-arch/bot-api": "workspace:*",
"@coze-arch/bot-error": "workspace:*",
"@coze-arch/bot-flags": "workspace:*",
"@coze-arch/bot-hooks": "workspace:*",
"@coze-arch/bot-http": "workspace:*",
"@coze-arch/bot-icons": "workspace:*",
"@coze-arch/bot-monaco-editor": "workspace:*",
"@coze-arch/bot-semi": "workspace:*",
"@coze-arch/bot-studio-store": "workspace:*",
"@coze-arch/bot-tea": "workspace:*",
"@coze-arch/bot-utils": "workspace:*",
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
"@coze-arch/foundation-sdk": "workspace:*",
"@coze-arch/i18n": "workspace:*",
"@coze-arch/logger": "workspace:*",
"@coze-common/assets": "workspace:*",
"@coze-common/biz-components": "workspace:*",
"@coze-community/components": "workspace:*",
"@coze-foundation/enterprise-store-adapter": "workspace:*",
"@coze-studio/bot-detail-store": "workspace:*",
"@coze-studio/bot-plugin-store": "workspace:*",
"@coze-studio/bot-utils": "workspace:*",
"@coze-studio/components": "workspace:*",
"@coze-studio/plugin-form-adapter": "workspace:*",
"@coze-studio/plugin-shared": "workspace:*",
"@coze-studio/premium-store-adapter": "workspace:*",
"@coze-studio/user-store": "workspace:*",
"@coze-workflow/base": "workspace:*",
"@douyinfe/semi-icons": "^2.36.0",
"@flowgram-adapter/free-layout-editor": "workspace:*",
"ahooks": "^3.7.8",
"axios": "^1.4.0",
"classnames": "^2.3.2",
"immer": "^10.0.3",
"lodash-es": "^4.17.21",
"yaml": "^2.2.2",
"zustand": "^4.4.7"
},
"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:*",
"@rsbuild/core": "1.1.13",
"@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/node": "18.18.9",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"@vitest/coverage-v8": "~3.0.5",
"less": "^3.13.1",
"react": "~18.2.0",
"react-dom": "~18.2.0",
"react-is": ">= 16.8.0",
"react-router-dom": "^6.22.0",
"scheduler": ">=0.19.0",
"styled-components": ">= 2",
"stylelint": "^15.11.0",
"typescript": "~5.8.2",
"vite": "^4.3.9",
"vite-plugin-svgr": "~3.3.0",
"vitest": "~3.0.5",
"webpack": "~5.91.0"
},
"peerDependencies": {
"react": ">=18.2.0",
"react-dom": ">=18.2.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

View File

@@ -0,0 +1,463 @@
/* stylelint-disable declaration-no-important */
/* stylelint-disable no-descending-specificity */
/* stylelint-disable max-nesting-depth */
/* stylelint-disable selector-class-pattern */
@import '@coze-common/assets/style/common.less';
@import '@coze-common/assets/style/mixins.less';
.plugin-item {
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
width: calc(100% - 68px);
margin: 14px 0 0 68px;
padding-right: 12px;
padding-bottom: 14px;
&:not(:last-child) {
position: relative;
border-bottom: 1px solid var(--light-usage-border-color-border, rgba(28, 31, 35, 8%));
}
.plugin-api-main {
flex: 1;
.plugin-api-name {
width: 100%;
:global {
.semi-typography {
font-size: 16px;
font-weight: 600;
line-height: 22px;
color: var(--light-usage-text-color-text-0, #1c1d23) !important;
}
}
}
.plugin-api-desc {
display: flex;
flex-direction: column;
width: 100%;
margin-top: 4px;
:global {
.semi-typography {
font-size: 12px;
line-height: 16px;
color: var(--light-usage-text-color-text-2,
rgba(28, 31, 35, 60%)) !important;
}
}
.api-params {
display: flex;
align-items: center;
margin-top: 12px;
.params-tags {
max-width: 500px;
.tag-item {
width: fit-content;
min-width: fit-content;
font-size: 12px;
font-weight: 500;
font-style: normal;
line-height: 16px;
color: var(--light-usage-text-color-text-2, rgba(28, 29, 35, 60%));
border-radius: 6px;
/* 133.333% */
}
&> :not(:first-child) {
margin-left: 16px;
}
}
.params-desc {
cursor: pointer;
flex-shrink: 0;
font-size: 12px;
line-height: 16px;
color: #4d53e8;
letter-spacing: 0.12px !important;
}
}
}
}
.plugin-api-method {
display: flex;
flex-shrink: 0;
align-items: center;
margin-left: 20px;
}
}
.plugin-panel-header {
display: flex;
flex: 1;
flex-wrap: nowrap;
align-items: center;
width: 0;
min-width: 0;
height: 80px;
font-weight: 400;
.creator-icon {
display: flex;
flex-shrink: 0;
border-radius: 50%;
img {
width: 14px;
height: 14px;
}
}
.creator-time {
/* Paragraph/small/EN-Regular */
font-size: 12px;
font-weight: 400;
font-style: normal;
line-height: 16px;
color: var(--light-usage-text-color-text-3, rgba(28, 29, 35, 35%));
text-align: right;
/* 133.333% */
letter-spacing: 0.12px;
}
.header-icon {
display: flex;
flex-shrink: 0;
img {
width: 36px;
height: 36px;
background: #fff;
}
}
.header-main {
overflow: hidden;
flex: 1;
width: 0;
min-width: 0;
margin: 0 16px;
// margin-top: -12px;
.header-name {
display: flex;
align-items: center;
width: 100%;
:global {
.semi-typography {
font-size: 16px;
font-weight: 600;
line-height: 22px;
color: var(--light-usage-text-color-text-0, #1c1d23) !important;
word-wrap: break-word !important;
}
.semi-highlight-tag {
color: #fda633;
background-color: transparent;
}
}
.market-link-icon {
display: none;
}
}
.header-desc {
width: 100%;
:global {
.semi-typography {
font-size: 12px;
font-weight: 400;
color: var(--light-usage-text-color-text-1,
rgba(28, 29, 35, 80%)) !important;
letter-spacing: 0.12px;
word-wrap: break-word !important;
}
}
}
}
.header-info {
/* Paragraph/small/EN-Regular */
font-size: 12px;
font-weight: 400;
font-style: normal;
line-height: 16px;
color: var(--light-usage-text-color-text-3, rgba(28, 29, 35, 35%));
text-align: right;
/* 133.333% */
:global {
.semi-divider-vertical {
height: 10px;
color: var(--light-usage-border-color-border-1, rgba(28, 29, 35, 12%));
}
}
}
}
.plugin-content {
overflow: auto;
width: 100%;
height: 100%;
padding-bottom: 12px;
.loading-more {
text-align: center;
}
.plugin-content-filter {
display: flex;
padding: 0 36px;
padding-left: 22px;
.plugin-content-sort {
width: 150px;
}
.bot-tag {
display: flex;
align-items: center;
}
:global {
.semi-tabs-content {
padding: 0;
}
.semi-tabs-tab-button.semi-tabs-tab-active {
color: var(--light-usage-text-color-text-1, rgba(28, 29, 35, 80%));
background-color: transparent;
.semi-icon {
.common-svg-icon(20px, rgba(28, 29, 35, 0.8));
}
}
.semi-tabs-tab-single.semi-tabs-tab-active .semi-icon:not(.semi-icon-checkbox_tick,
.semi-icon-radio,
.semi-icon-checkbox_indeterminate) {
top: 0;
color: var(--light-usage-text-color-text-1, rgba(28, 29, 35, 80%));
}
.semi-tabs-tab-single.semi-tabs-tab .semi-icon:not(.semi-icon-checkbox_tick,
.semi-icon-radio,
.semi-icon-checkbox_indeterminate) {
top: 0;
}
.semi-tabs-tab:last-child::before {
content: '';
position: absolute;
top: 12px;
left: -4px;
width: 1px;
height: 16px;
background-color: var(--light-usage-border-color-border,
rgba(28, 29, 35, 12%));
}
}
}
.plugin-collapse {
width: 100%;
:global {
.semi-collapse-item {
position: relative;
border: none;
}
.semi-collapse-header:hover::before {
content: '';
position: absolute;
top: -1px;
left: 0;
width: 100%;
height: 1px;
background-color: #f7f7fa;
}
.semi-collapse-header {
margin: 0;
border-bottom: 1px solid #dfdfdf;
border-radius: 0;
&:hover {
background: var(--light-usage-fill-color-fill-0,
rgba(46, 47, 56, 5%));
border-bottom: 1px solid transparent;
border-radius: 8px;
}
&:active {
background: var(--light-usage-fill-color-fill-0,
rgba(46, 47, 56, 5%));
}
}
.semi-collapse-header-icon {
width: auto;
height: 24px;
&:hover {
background: var(--light-usage-fill-color-fill-1,
rgba(46, 47, 56, 9%));
border-radius: 5px;
}
}
}
:global(.semi-collapse-header) {
&:hover {
.market-link-icon {
display: block;
}
}
}
.item-container {
padding: 0;
}
.collapse-icon {
.common-svg-icon(16px, rgba(28, 29, 35, 0.35));
cursor: pointer;
padding: 4px;
}
.activePanel {
margin-bottom: 8px;
background: var(--light-usage-fill-color-fill-0, rgba(46, 47, 56, 5%));
border: none;
border-radius: 8px;
:global {
.semi-collapse-header {
border-bottom: 1px solid #dfdfdf;
}
.semi-collapse-header:hover {
background: transparent;
}
}
}
}
}
.plugin-content,
.plugin-collapse {
:global {
.semi-collapse-header {
min-width: 870px;
height: 140px !important;
margin: 0 !important;
padding: 14px 16px;
&[aria-expanded='true'] {
border-radius: 8px 8px 0 0 !important;
}
}
.semi-collapse {
padding: 16px 0 12px;
}
.semi-collapse-content {
// background-color: #fff;
padding: 0;
border-radius: 0 0 8px 8px;
// border-color: var(
// --light-usage-border-color-border,
// rgba(28, 31, 35, 0.08)
// );
// border-width: 0 1px 1px 1px;
// border-style: solid;
}
.semi-collapse-item {
// border: 0;
}
}
}
button.operator-btn {
width: 98px;
&.added {
color: var(--light-usage-primary-color-primary-disabled, #b4baf6);
background: var(--light-usage-bg-color-bg-0, #fff);
border: 1px solid var(--light-usage-disabled-color-disabled-border, #f0f0f5);
}
&.addedMouseIn {
color: var(--light-color-red-red-5, #ff441e);
background: #fff;
border: 1px solid var(--light-usage-border-color-border-1, rgba(29, 28, 35, 12%));
}
}
.workflow_count_span {
display: inline-block;
width: 16px;
height: 16px;
margin-left: 6px;
font-size: 10px;
line-height: 17px;
color: #fff;
vertical-align: 1px;
background-color: rgba(77, 83, 232, 100%);
border-radius: 8px;
}
.store-plugin-tools {
display: flex;
margin-top: 8px;
font-size: 12px;
color: var(--light-usage-text-color-text-3, rgba(28, 29, 35, 35%));
}
.plugin-total {
margin-top: 4px;
margin-bottom: 4px;
font-size: 12px;
color: var(--light-usage-text-color-text-3, rgba(28, 29, 35, 35%));
}

View File

@@ -0,0 +1,242 @@
/*
* 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 */
import { type FC, useRef, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { sortBy } from 'lodash-es';
import classNames from 'classnames';
import { useUpdateEffect } from 'ahooks';
import { IconChevronDown, IconChevronRight } from '@douyinfe/semi-icons';
import { useWorkflowStore } from '@coze-workflow/base/store';
import { InfiniteList, type InfiniteListRef } from '@coze-community/components';
import { I18n } from '@coze-arch/i18n';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { UICompositionModalMain, Collapse } from '@coze-arch/bot-semi';
import {
type PluginApi,
type PluginInfoForPlayground,
} from '@coze-arch/bot-api/plugin_develop';
import {
fetchPlugin,
formatCacheKey,
type PluginContentListItem,
type PluginQuery,
type PluginModalModeProps,
} from '@coze-agent-ide/plugin-shared';
import { PluginPanel } from '@coze-agent-ide/plugin-modal-adapter';
import { useInfiniteScrollCacheLoad } from '../use-request-cache';
import s from './index.module.less';
export interface PluginModalContentProps extends PluginModalModeProps {
query: PluginQuery;
pluginApiList: PluginApi[];
onPluginApiListChange: (list: PluginApi[]) => void;
setQuery: (value: Partial<PluginQuery>, refreshPage?: boolean) => void;
}
export type PluginModalContentListItem = PluginInfoForPlayground & {
// 当前数据属于列表的第几页
belong_page?: number;
};
// @ts-expect-error -- linter-disable-autofix
// eslint-disable-next-line max-params
const getEmptyConf = (spaceId, isMine, isTeam, isProject) => {
if ((isMine || isTeam) && !isProject) {
return {
text: {
emptyTitle: I18n.t('plugin_empty_desc'),
emptyDesc: I18n.t('plugin_empty_description'),
},
btn: {
emptyClick: () => {
window.open(`/space/${spaceId}/library?type=1`);
},
emptyText: I18n.t('plugin_create'),
},
};
}
return {
text: {
emptyTitle: I18n.t('plugin_empty_desc'),
emptyDesc: '',
},
btn: {
emptyClick: () => {
window.open('/store/plugin');
},
emptyText: I18n.t('mkl_plugin_to_plugin_gallery'),
},
};
};
/* eslint-disable @coze-arch/max-line-per-function */
export const PluginModalContent: FC<PluginModalContentProps> = ({
query,
pluginApiList,
onPluginApiListChange,
openMode,
from,
openModeCallback,
showButton,
showCopyPlugin,
onCopyPluginCallback,
clickProjectPluginCallback,
}) => {
// 状态hook
const {
type,
mineActive,
search,
isOfficial,
orderBy,
orderByPublic,
orderByFavorite,
agentId,
pluginType,
} = query;
const id = useSpaceStore(store => store.space.id);
// scroll的container
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
// 当前active的key
const [activeKey, setActivekey] = useState<string | string[] | undefined>([]);
const refInfiniteScroll = useRef<InfiniteListRef>(null);
const {
scroll2Top,
loadData,
isSearching,
isFavorite,
isTemplate,
isProject,
isMine,
isTeam,
} = useInfiniteScrollCacheLoad<PluginContentListItem, PluginQuery>({
query,
formatCacheKey,
scrollContainer: scrollContainerRef,
triggerService: fetchPlugin,
onSetScrollData: scrollData => {
refInfiniteScroll.current?.mutate(scrollData);
},
});
const { nodes: workflowNodes } = useWorkflowStore(
useShallow(state => ({
nodes: state.nodes,
})),
);
// 首次effect不执行这个是切换状态的effect
useUpdateEffect(() => {
scroll2Top(); // 当筛选项改变时,回到顶部
// 只要是query中非page改变就执行此effect
}, []);
return (
<UICompositionModalMain>
<div className={s['plugin-content']} ref={scrollContainerRef}>
<UICompositionModalMain.Content
style={{ minHeight: '100%', display: 'flex' }}
>
<Collapse
className={s['plugin-collapse']}
activeKey={activeKey}
onChange={value => {
setActivekey(value);
}}
expandIcon={
<IconChevronRight
className={s['collapse-icon']}
data-testid="plugin-collapse-panel-expand"
/>
}
collapseIcon={
<IconChevronDown
className={s['collapse-icon']}
data-testid="plugin-collapse-panel-collapse"
/>
}
>
<InfiniteList<PluginContentListItem>
ref={refInfiniteScroll}
itemClassName={s['item-container']}
renderItem={(item, index) => {
const pluginId = item?.pluginInfo?.id;
return (
<PluginPanel
agentId={agentId}
index={index}
pluginApiList={pluginApiList}
onPluginApiListChange={onPluginApiListChange}
onCopyPluginCallback={onCopyPluginCallback}
showButton={showButton}
showCopyPlugin={showCopyPlugin}
openMode={openMode}
from={from}
workflowNodes={workflowNodes}
openModeCallback={openModeCallback}
highlightWords={[search]}
showCreator={true}
showMarketLink={isFavorite || isTemplate}
showCreateTime={orderBy === 0 || typeof type === 'number'}
showPublishTime={!isMine && !isTeam && !isProject}
activeKey={activeKey}
scrollContainerRef={scrollContainerRef}
isFromMarket={item?.isFromMarket}
info={{
...item?.pluginInfo,
id: pluginId,
listed_at: item?.productInfo?.listed_at,
plugin_apis: sortBy(
item?.pluginInfo?.plugin_apis,
p => p.name,
),
}}
productInfo={item?.productInfo}
commercialSetting={item?.commercial_setting}
key={pluginId}
type={String(type || '')}
className={classNames(s['plugin-collapse'], {
[s.activePanel]: activeKey?.includes(pluginId ?? ''),
})}
showProjectPluginLink={isProject}
clickProjectPluginCallback={clickProjectPluginCallback}
/>
);
}}
emptyConf={getEmptyConf(id, isMine, isTeam, isProject)}
scrollConf={{
reloadDeps: [
type,
mineActive,
search,
isOfficial,
orderBy,
orderByPublic,
orderByFavorite,
pluginType,
],
targetRef: scrollContainerRef,
loadData,
}}
isSearching={isSearching}
/>
</Collapse>
</UICompositionModalMain.Content>
</div>
</UICompositionModalMain>
);
};

View File

@@ -0,0 +1,184 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { userStoreService } from '@coze-studio/user-store';
import { useCollaborationStore } from '@coze-studio/bot-detail-store/collaboration';
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import {
PluginType,
ProductEntityType,
SortType,
} from '@coze-arch/bot-api/product_api';
import {
OrderBy,
SpaceType,
type PluginApi,
} from '@coze-arch/bot-api/developer_api';
import {
DEFAULT_PAGE,
MineActiveEnum,
PluginFilterType,
type PluginQuery,
type PluginModalModeProps,
From,
} from '@coze-agent-ide/plugin-shared';
import { PluginModalFilter } from '@coze-agent-ide/plugin-modal-adapter';
import { PluginModalSider } from './sider';
import { PluginModalContent } from './content';
export interface UsePluginModalPartsProp extends PluginModalModeProps {
pluginApiList: PluginApi[];
onPluginApiListChange: (list: PluginApi[]) => void;
agentId?: string;
projectId?: string;
isShowStorePlugin?: boolean;
}
/**
* 获取初始化类型
* @param from 来源
* @param spaceType 空间类型
* @returns 初始化类型
*/
const getInitType = (from?: From, spaceType?: SpaceType) => {
// 项目workflow引用插件默认选中项目插件
if (from === From.ProjectWorkflow) {
return '';
}
if (from !== From.ProjectIde || !spaceType || !from) {
return '';
}
// projectIDE下并且是个人空间选中Mine
if (spaceType === SpaceType.Personal) {
return PluginFilterType.Mine;
}
// projectIDE下并且是团队空间选中Team
if (spaceType === SpaceType.Team && from === From.ProjectIde) {
return PluginFilterType.Team;
}
return '';
};
export const usePluginModalParts = ({
pluginApiList,
onPluginApiListChange,
agentId,
openMode,
from,
openModeCallback,
showButton,
showCopyPlugin,
onCopyPluginCallback,
projectId,
clickProjectPluginCallback,
onCreateSuccess,
isShowStorePlugin,
hideCreateBtn,
initQuery,
}: UsePluginModalPartsProp) => {
// 获取devId
const userInfo = userStoreService.useUserInfo();
const spaceType = useSpaceStore(store => store.space.space_type);
const [query, setQuery] = useState<PluginQuery>({
agentId,
projectId,
devId: userInfo?.user_id_str || '',
search: '',
page: DEFAULT_PAGE,
// 项目IDE插件仅展示我的插件
type: initQuery?.type ?? getInitType(from, spaceType),
orderBy: OrderBy.CreateTime,
orderByPublic: SortType.Heat,
orderByFavorite: SortType.Newest,
mineActive: MineActiveEnum.All,
isOfficial: initQuery?.isOfficial ?? undefined,
// project workflow添加插件只展示云插件
pluginType:
from === From.ProjectWorkflow ? PluginType.CLoudPlugin : undefined,
});
const { botId } = useBotInfoStore(
useShallow(state => ({
botId: state.botId,
})),
);
const { version } = useCollaborationStore(
useShallow(state => ({
version: state.baseVersion,
})),
);
const updateQuery = (value: Partial<PluginQuery>, refreshPage = true) => {
const botInfo = {
current_entity_type: ProductEntityType.Bot,
current_entity_id: botId,
current_entity_version: version,
};
setQuery(prev => {
if (refreshPage) {
return {
...prev,
...value,
page: DEFAULT_PAGE,
botInfo,
};
}
return {
...prev,
...value,
botInfo,
};
});
};
const sider = (
<PluginModalSider
hideCreateBtn={hideCreateBtn}
query={query}
setQuery={updateQuery}
from={from}
onCreateSuccess={onCreateSuccess}
isShowStorePlugin={isShowStorePlugin}
/>
);
const filter = (
<PluginModalFilter from={from} query={query} setQuery={updateQuery} />
);
const content = (
<PluginModalContent
query={query}
setQuery={updateQuery}
pluginApiList={pluginApiList}
onPluginApiListChange={onPluginApiListChange}
openMode={openMode}
from={from}
openModeCallback={openModeCallback}
showButton={showButton}
showCopyPlugin={showCopyPlugin}
onCopyPluginCallback={onCopyPluginCallback}
clickProjectPluginCallback={clickProjectPluginCallback}
/>
);
return {
sider,
content,
filter,
} as const;
};

View File

@@ -0,0 +1,4 @@
button.addbtn {
width: 100%;
margin-top: 24px;
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState, type FC } from 'react';
import { useDebounceFn } from 'ahooks';
import { UISearch } from '@coze-studio/components';
import { I18n } from '@coze-arch/i18n';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { UIButton, UICompositionModalSider } from '@coze-arch/bot-semi';
import { From, type PluginQuery } from '@coze-agent-ide/plugin-shared';
import { PluginFilter } from '@coze-agent-ide/plugin-modal-adapter';
import { CreateFormPluginModal } from '../../bot_edit';
import s from './index.module.less';
export interface PluginModalSiderProp {
query: PluginQuery;
setQuery: (value: Partial<PluginQuery>, refreshPage?: boolean) => void;
from?: From;
onCreateSuccess?: (val?: { spaceId?: string; pluginId?: string }) => void;
isShowStorePlugin?: boolean;
hideCreateBtn?: boolean;
}
const MAX_SEARCH_LENGTH = 100;
export const PluginModalSider: FC<PluginModalSiderProp> = ({
query,
setQuery,
from,
onCreateSuccess,
isShowStorePlugin,
hideCreateBtn,
}) => {
const [showFormPluginModel, setShowFormPluginModel] = useState(false);
const id = useSpaceStore(item => item.space.id);
const updateSearchQuery = (search?: string) => {
setQuery({
search: search ?? '',
});
};
const { run: debounceChangeSearch, cancel } = useDebounceFn(
(search: string) => {
updateSearchQuery(search);
},
{ wait: 300 },
);
return (
<>
{hideCreateBtn ? null : (
<CreateFormPluginModal
projectId={query.projectId}
isCreate={true}
visible={showFormPluginModel}
onSuccess={pluginID => {
onCreateSuccess?.({
spaceId: id,
pluginId: pluginID,
});
}}
onCancel={() => {
setShowFormPluginModel(false);
}}
/>
)}
<UICompositionModalSider style={{ paddingTop: 16 }}>
<UICompositionModalSider.Header>
<UISearch
tabIndex={-1}
value={query.search}
maxLength={MAX_SEARCH_LENGTH}
onSearch={search => {
if (!search) {
// 如果search清空了那么立即更新query
cancel();
updateSearchQuery(search);
} else {
// 如果search有值那么防抖更新
debounceChangeSearch(search);
}
}}
placeholder={I18n.t('Search')}
data-testid="plugin.modal.search"
/>
{hideCreateBtn ? null : (
<UIButton
data-testid="plugin.modal.create.plugin"
className={s.addbtn}
theme="solid"
onClick={() => {
// TODO: 其他场景应该也统一创建方式如果创建成功回调存在则打开插件modal否则打开新tab
if (
onCreateSuccess &&
(from === From.ProjectIde || from === From.ProjectWorkflow)
) {
setShowFormPluginModel(true);
return;
}
window.open(`/space/${id}/library?type=1`);
}}
>
{I18n.t('plugin_create')}
</UIButton>
)}
</UICompositionModalSider.Header>
<UICompositionModalSider.Content>
<PluginFilter
isSearching={query.search !== ''}
type={query.type}
onChange={type => {
setQuery({ type });
}}
from={from}
projectId={query.projectId}
isShowStorePlugin={isShowStorePlugin}
/>
</UICompositionModalSider.Content>
</UICompositionModalSider>
</>
);
};

View File

@@ -0,0 +1,205 @@
/*
* 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 MutableRefObject, useEffect } from 'react';
import { setCache, getCache, clearCache } from '@coze-arch/bot-utils';
import { type InfiniteListDataProps } from '@coze-community/components';
import {
DEFAULT_PAGE_SIZE,
MineActiveEnum,
PluginFilterType,
type CommonQuery,
type ListItemCommon,
type RequestServiceResp,
} from '@coze-agent-ide/plugin-shared';
interface Data {
list: object[];
total: number;
hasMore?: boolean;
}
export interface InfiniteScrollViewportOptions<
ListItem extends ListItemCommon,
Query extends CommonQuery,
> {
scrollContainer: MutableRefObject<HTMLDivElement | null>;
query: Query;
triggerService: (
query: Query,
commonParam: {
nextPage: number;
isMine: boolean;
isTeam: boolean;
isCreatorMine: boolean;
isTemplate: boolean;
isFavorite: boolean;
isProject: boolean;
},
) => Promise<RequestServiceResp<ListItem> | undefined>;
// @ts-expect-error -- linter-disable-autofix
onSetScrollData: (scrollData) => void;
formatCacheKey: (query: {
query: Query;
isSearching: boolean;
isTemplate: boolean;
page: number;
}) => string | undefined;
}
const DEFAULT_CACHE_TIME = 300000;
export function useInfiniteScrollCacheLoad<
ListItem extends ListItemCommon,
Query extends CommonQuery,
>({
scrollContainer,
query,
triggerService,
formatCacheKey,
onSetScrollData,
}: InfiniteScrollViewportOptions<ListItem, Query>) {
const { search, type, mineActive } = query;
const isSearching = search !== '';
// my tools
const isMine = type === PluginFilterType.Mine;
// team tools
const isTeam = type === PluginFilterType.Team;
const isFavorite = type === PluginFilterType.Favorite;
const isProject = type === PluginFilterType.Project;
// team tools -> my creator
const isCreatorMine = mineActive === MineActiveEnum.Mine;
const isTemplate = Number(type) >= 0 || type === 'recommend';
const scroll2Top = () => {
if (scrollContainer.current) {
scrollContainer.current.scrollTo({
top: 0,
});
}
};
const onBeforeLoadData = (
// @ts-expect-error -- linter-disable-autofix
current,
cachedKey: string,
isImmediateUpdate: boolean,
) => {
const res = getCache(cachedKey);
if (!cachedKey || !res) {
return false;
}
const { data } = res;
if (!isImmediateUpdate) {
return res.data;
}
const currentPage = current?.nextPage || 1;
const { list, total } = (data as Data) || { list: [], total: 0 };
const hasMore = total > 0 && currentPage * DEFAULT_PAGE_SIZE < total;
onSetScrollData({
...current,
hasMore,
list: [...(currentPage?.list || []), ...list],
});
return false;
};
const setCacheData = <TData,>(
cachedKey: string,
cacheTime: number,
res: TData,
) => {
if (!cachedKey) {
return;
}
setCache(cachedKey, cacheTime, {
time: Date.now(),
data: res,
});
};
const loadData = async (current: InfiniteListDataProps<ListItem>) => {
const currentPage = current?.nextPage || 1;
let cachedKey =
formatCacheKey({ query, isSearching, isTemplate, page: currentPage }) ||
'';
if (!isMine && !isTeam) {
cachedKey = '';
}
let res = onBeforeLoadData(current, cachedKey, !isTemplate);
if (!res) {
res = await triggerService(query, {
nextPage: currentPage,
isMine,
isTeam,
isCreatorMine,
isTemplate,
isFavorite,
isProject,
});
setCacheData(cachedKey, DEFAULT_CACHE_TIME, res);
}
const { list, hasMore } = (res as InfiniteListDataProps<ListItem>) || {
list: [],
total: 0,
};
const nextPage = currentPage + 1;
const refIdList = {};
(current?.list || []).map(item => {
// @ts-expect-error -- linter-disable-autofix
refIdList[
(item as unknown as { pluginInfo: { id: string } })?.pluginInfo?.id
] = true;
});
//数据去重
const uniqList = (list || []).filter(item => {
const pluginId = (item as unknown as { pluginInfo: { id: string } })
?.pluginInfo?.id;
if (pluginId) {
// @ts-expect-error -- linter-disable-autofix
if (refIdList[pluginId]) {
return false;
}
}
return true;
});
return {
list: uniqList || [],
hasMore,
nextPage,
};
};
useEffect(() => {
clearCache();
}, []);
return {
scroll2Top,
isSearching,
loadData,
isFavorite,
isTemplate,
isMine,
isTeam,
isProject,
};
}

View File

@@ -0,0 +1,179 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState, useEffect, type ReactNode } from 'react';
import classNames from 'classnames';
import { type PluginInfoProps } from '@coze-studio/plugin-shared';
import { I18n } from '@coze-arch/i18n';
import { safeJSONParse } from '@coze-arch/bot-utils';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { UIButton, UIModal, Toast, Space } from '@coze-arch/bot-semi';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import { Editor } from '../editor';
import s from './index.module.less';
export interface CreatePluginProps {
visible: boolean;
isCreate?: boolean;
editInfo?: PluginInfoProps;
disabled?: boolean;
onCancel?: () => void;
onSuccess?: (pluginId?: string) => void;
actions?: ReactNode;
projectId?: string;
}
const INDENTATION_SPACES = 2;
const EDITOR_HEIGHT_MAX = 560;
export const CreateCodePluginModal: React.FC<CreatePluginProps> = props => {
const {
isCreate = true,
onCancel,
editInfo,
visible,
onSuccess,
disabled = false,
actions,
projectId,
} = props;
const [aiPlugin, setAiPlugin] = useState<string | undefined>();
const [clientId, setClientId] = useState<string | undefined>();
const [clientSecret, setClientSecret] = useState<string | undefined>();
const [serviceToken, setServiceToken] = useState<string | undefined>();
const [openApi, setOpenApi] = useState<string | undefined>();
useEffect(() => {
/** 每次打开重置弹窗数据 */
if (visible) {
//格式化json
const desc = JSON.stringify(
safeJSONParse(editInfo?.code_info?.plugin_desc),
null,
INDENTATION_SPACES,
);
setAiPlugin(desc || '');
setOpenApi(editInfo?.code_info?.openapi_desc || '');
setClientId(editInfo?.code_info?.client_id);
setClientSecret(editInfo?.code_info?.client_secret);
setServiceToken(editInfo?.code_info?.service_token);
}
}, [visible]);
const registerPlugin = async () => {
const params = {
ai_plugin: aiPlugin,
client_id: clientId,
client_secret: clientSecret,
service_token: serviceToken,
openapi: openApi,
};
let res;
if (isCreate) {
res = await PluginDevelopApi.RegisterPlugin({
...params,
project_id: projectId,
space_id: useSpaceStore.getState().getSpaceId(),
});
} else {
await PluginDevelopApi.UpdatePlugin({
...params,
plugin_id: editInfo?.plugin_id,
edit_version: editInfo?.edit_version,
});
}
Toast.success({
content: isCreate
? I18n.t('register_success')
: I18n.t('Plugin_update_success'),
showClose: false,
});
onSuccess?.(res?.data?.plugin_id);
onCancel?.();
};
return (
<UIModal
fullScreen
className="full-screen-modal"
title={
<div className={s['bot-code-edit-title-action']}>
<span>
{isCreate ? I18n.t('plugin_create') : I18n.t('plugin_Update')}
</span>
<div>{actions}</div>
</div>
}
visible={visible}
onCancel={() => onCancel?.()}
footer={
!disabled ? (
<Space>
<UIButton type="tertiary" onClick={() => onCancel?.()}>
{I18n.t('Cancel')}
</UIButton>
<UIButton type="primary" onClick={registerPlugin}>
{I18n.t('Confirm')}
</UIButton>
</Space>
) : null
}
maskClosable={false}
>
<div className={classNames(s.flex)}>
<div className={classNames(s['plugin-height'], s.flex5)}>
<div style={{ display: 'flex' }}>
<div style={{ flex: 1, borderRight: '1px solid rgb(215,218,221)' }}>
<div className={s.title}>
{I18n.t('ai_plugin_(fill_in_json)_*')}
</div>
<Editor
dataTestID="create-plugin-code-editor-json"
disabled={disabled}
theme="tomorrow"
mode="json"
height={EDITOR_HEIGHT_MAX}
value={aiPlugin}
useValidate={false}
onChange={e => setAiPlugin(e)}
/>
</div>
<div style={{ flex: 1 }}>
<div className={s.title}>
{I18n.t('openapi_(fill_in_yaml)_*')}
</div>
<Editor
dataTestID="create-plugin-code-editor-yaml"
disabled={disabled}
theme="tomorrow"
mode="yaml"
height={EDITOR_HEIGHT_MAX}
value={openApi}
useValidate={false}
onChange={e => setOpenApi(e)}
/>
</div>
</div>
</div>
</div>
</UIModal>
);
};

View File

@@ -0,0 +1,54 @@
/*
* 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 { Button } from '@coze-arch/coze-design';
import { IconCodeOutlined } from '@coze-arch/bot-icons';
import { CreateCodePluginModal } from '../bot-code-edit';
export const CodeModal: FC<{
onCancel?: () => void;
onSuccess?: (pluginId?: string) => void;
projectId?: string;
}> = ({ onCancel, onSuccess, projectId }) => {
const [showCodePluginModel, setShowCodePluginModel] = useState(false);
return (
<>
<CreateCodePluginModal
isCreate={true}
visible={showCodePluginModel}
onSuccess={pluginId => {
onSuccess?.(pluginId);
}}
onCancel={() => {
setShowCodePluginModel(false);
}}
projectId={projectId}
/>
<Button
data-testid="create-plugin-code-modal-button"
color="primary"
icon={<IconCodeOutlined />}
onClick={() => {
setShowCodePluginModel(true);
onCancel?.();
}}
/>
</>
);
};

View File

@@ -0,0 +1,58 @@
/*
* 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 { Button } from '@coze-arch/coze-design';
import { ImportPluginModal } from '../../file-import';
export const ImportModal: FC<{
onCancel?: () => void;
onSuccess?: (pluginID?: string) => void;
projectId?: string;
}> = ({ onCancel, onSuccess, projectId }) => {
const [showFileImportPluginModel, setShowFileImportPluginModel] =
useState(false);
return (
<>
<ImportPluginModal
projectId={projectId}
visible={showFileImportPluginModel}
onSuccess={d => {
const pluginId = d?.plugin_id;
if (pluginId) {
onSuccess?.(pluginId);
} else {
onSuccess?.();
}
}}
onCancel={() => setShowFileImportPluginModel(false)}
/>
<Button
color="primary"
onClick={() => {
setShowFileImportPluginModel(true);
onCancel?.();
}}
>
{I18n.t('import')}
</Button>
</>
);
};

View File

@@ -0,0 +1,250 @@
/*
* 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 FC, useMemo, useState, useEffect } from 'react';
import { type PluginInfoProps } from '@coze-studio/plugin-shared';
import {
PluginForm,
usePluginFormState,
convertPluginMetaParams,
registerPluginMeta,
updatePluginMeta,
} from '@coze-studio/plugin-form-adapter';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { I18n } from '@coze-arch/i18n';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import {
type CreationMethod,
type PluginType,
} from '@coze-arch/bot-api/plugin_develop';
import { ERROR_CODE } from '@coze-agent-ide/bot-plugin-tools/pluginModal/types';
import { IconCozInfoCircleFill } from '@coze-arch/coze-design/icons';
import {
Button,
Divider,
Modal,
Space,
Toast,
Typography,
} from '@coze-arch/coze-design';
import s from '../index.module.less';
import { PluginDocs } from '../../plugin-docs';
import { ImportModal } from './import-modal';
import { CodeModal } from './code-modal';
export interface CreatePluginFormProps {
visible: boolean;
isCreate?: boolean;
editInfo?: PluginInfoProps;
disabled?: boolean;
onCancel?: () => void;
onSuccess?: (pluginID?: string) => Promise<void> | void;
projectId?: string;
}
export const CreateFormPluginModal: FC<CreatePluginFormProps> = props => {
const {
onCancel,
editInfo,
isCreate = true,
visible,
onSuccess,
disabled = false,
projectId,
} = props;
const { id } = useSpaceStore(store => store.space);
const modalTitle = useMemo(() => {
if (isCreate) {
return (
<div className="w-full flex justify-between items-center pr-[8px]">
<div>{I18n.t('create_plugin_modal_title1')}</div>
<Space>
<CodeModal
onCancel={onCancel}
onSuccess={onSuccess}
projectId={projectId}
/>
<ImportModal
onCancel={onCancel}
onSuccess={onSuccess}
projectId={projectId}
/>
<Divider layout="vertical" className="h-5" />
</Space>
</div>
);
}
if (disabled) {
return I18n.t('plugin_detail_view_modal_title');
}
return I18n.t('plugin_detail_edit_modal_title');
}, [isCreate, disabled]);
const [loading, setLoading] = useState(false);
const pluginState = usePluginFormState();
const {
formApi,
extItems,
headerList,
isValidCheckResult,
setIsValidCheckResult,
pluginTypeCreationMethod,
defaultRuntime,
} = pluginState;
useEffect(() => {
if (!isCreate) {
return;
}
if (visible) {
// 显示后滚动条滑动到最上边
const modalContent = document.querySelector(
'.create-plugin-modal-content .semi-modal-body',
);
if (modalContent) {
modalContent.scrollTop = 0;
}
} else {
// 隐藏后重置表单
formApi?.current?.reset();
}
}, [visible]);
const confirmBtn = async () => {
await formApi.current?.validate();
const type = isCreate ? 'create' : 'edit';
const val = formApi.current?.getValues();
if (!val || !pluginTypeCreationMethod) {
return;
}
const json: Record<string, string> = {};
extItems?.forEach(item => {
if (item.key in val) {
json[item.key] = val[item.key];
}
});
const [pluginType, creationMethod] = pluginTypeCreationMethod.split('-');
const params = convertPluginMetaParams({
val,
spaceId: String(id),
headerList,
projectId,
creationMethod: Number(creationMethod) as unknown as CreationMethod,
defaultRuntime,
pluginType: Number(pluginType) as unknown as PluginType,
extItemsJSON: json,
});
const action = {
create: () => registerPluginMeta({ params }),
edit: () => updatePluginMeta({ params, editInfo }),
};
try {
setLoading(true);
const pluginID = await action[type]();
Toast.success({
content: isCreate
? I18n.t('Plugin_new_toast_success')
: I18n.t('Plugin_update_toast_success'),
showClose: false,
});
onCancel?.();
onSuccess?.(pluginID);
} catch (error) {
// @ts-expect-error -- linter-disable-autofix
const { code, msg } = error;
if (Number(code) === ERROR_CODE.SAFE_CHECK) {
setIsValidCheckResult(false);
} else {
Toast.error({
content: withSlardarIdButton(msg),
});
}
} finally {
setLoading(false);
}
};
return (
<>
<Modal
title={modalTitle}
className="[&_.semi-modal-header]:items-center"
visible={visible}
keepDOM={isCreate}
onCancel={() => onCancel?.()}
modalContentClass="create-plugin-modal-content"
footer={
!disabled && (
<div>
{!isValidCheckResult && (
<div className={s['error-msg-box']}>
<span className={s['error-msg']}>
{I18n.t('plugin_create_modal_safe_error')}
</span>
</div>
)}
<Typography.Paragraph
type="secondary"
fontSize="12px"
className="text-start mb-[16px]"
>
<IconCozInfoCircleFill className="coz-fg-hglt text-[14px] align-sub" />
<span className="mx-[4px]">
{I18n.t('plugin_create_draft_desc')}
</span>
<PluginDocs />
</Typography.Paragraph>
<div>
<Button
color="primary"
onClick={() => {
onCancel?.();
}}
>
{I18n.t('create_plugin_modal_button_cancel')}
</Button>
<Button
loading={loading}
onClick={() => {
confirmBtn();
}}
>
{I18n.t('create_plugin_modal_button_confirm')}
</Button>
</div>
</div>
)
}
>
<PluginForm
pluginState={pluginState}
visible={visible}
isCreate={isCreate}
disabled={disabled}
editInfo={editInfo}
/>
</Modal>
</>
);
};

View File

@@ -0,0 +1,371 @@
/* stylelint-disable declaration-no-important */
/* stylelint-disable no-descending-specificity */
.card {
cursor: pointer;
position: relative;
min-width: 248px;
height: 172px;
background-color: white !important;
border-radius: 8px;
transition: box-shadow 0.4s;
&:hover {
box-shadow: 0 4px 20px 0 rgb(31 35 41 / 4%),
0 4px 10px 0 rgb(31 35 41 / 4%),
0 2px 5px 0 rgb(31 35 41 / 4%);
}
}
.card-favorite-not-publish {
cursor: not-allowed;
background-color: var(--light-usage-fill-color-fill-0,
rgb(46 50 56 / 5%)) !important;
}
.add-card {
background-color: white;
border-radius: 8px;
}
.add-card-inner {
display: flex;
flex-direction: column;
justify-content: center;
}
.name-wrap {
display: flex;
align-items: center;
width: 100%;
height: 40px;
padding: 16px 16px 0;
}
.avatar {
flex-shrink: 0;
width: 24px;
height: 24px;
border-radius: 50%px;
}
.name {
position: absolute;
top: 16px;
left: 48px;
overflow: hidden;
max-width: calc(100% - 156px);
font-size: 14px;
font-weight: 600;
line-height: 24px;
color: #000;
text-overflow: ellipsis;
white-space: nowrap;
}
.extra {
margin-left: auto;
}
.card-content {
display: flex;
flex-direction: column;
height: calc(100% - 40px);
padding: 16px;
}
.description {
overflow: hidden;
display: -webkit-box;
font-size: 12px;
line-height: 18px;
color: #494c4f;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.recent-modify {
margin-top: 8px;
font-size: 12px;
line-height: 16px;
color: rgb(28 31 35 / 60%);
}
.creator {
width: fit-content;
padding: 4px;
font-size: 12px;
line-height: 16px;
color: #346ef8;
background: rgb(51 112 255 / 10%);
border: none !important;
border-radius: 3px !important;
}
.upload-form {
display: flex;
flex-direction: column;
gap: 16px;
.upload-field {
padding-top: 0;
:global {
.semi-form-field-help-text {
justify-content: center;
}
}
}
.textarea-single-line {
:global {
.semi-input-textarea-counter {
position: absolute;
top: 6px;
right: 0;
}
}
}
.textarea-multi-line {
margin-bottom: 16px;
:global {
.semi-input-textarea-counter {
position: absolute;
right: 0;
bottom: -20px;
min-height: 0;
padding: 0;
}
}
}
.footer-draft {
align-items: flex-start;
padding-top: 16px;
font-size: 12px;
line-height: 16px;
color: var(--coz-fg-secondary);
.link {
font-weight: 400;
color: var(--coz-fg-hglt);
}
:global {
.semi-icon {
margin-top: 2px;
}
}
}
:global {
.semi-form-field {
padding: 0;
}
input::-webkit-contacts-auto-fill-button {
pointer-events: none;
position: absolute;
right: 0;
display: none !important;
visibility: hidden;
}
}
}
.upload-form-item {
:global {
.semi-form-field-label-text {
display: none;
}
}
}
.collect-num {
width: 12px;
height: 12px;
margin-right: 4px;
svg {
width: 12px;
height: 12px;
}
}
.user-info {
margin-top: auto;
}
.extinfo {
max-width: 338px;
font-size: 12px;
.extinfo-title {
font-weight: 700;
}
.extinfo-text {
color: rgb(28 31 35 / 60%);
}
.extinfo-ex {
margin-top: 4px;
padding: 6px 10px;
color: rgb(28 31 35 / 60%);
border: 1px solid rgb(28 31 35 / 8%);
}
}
.upload-avatar {
flex-shrink: 0;
width: 80px !important;
height: 80px !important;
background: #fff !important;
border-radius: var(--spacing-tight, 8px) !important;
}
.header-list {
:global {
.semi-form-field-label-with-extra {
padding-right: 0;
}
.semi-form-field-label-extra {
flex: 1;
}
}
.header-list-extra {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.header-list-box {
overflow: auto;
max-height: 348px;
border: 1px solid var(--coz-stroke-primary);
border-radius: 8px;
.header-row {
border-bottom: 1px solid var(--coz-stroke-primary);
}
.header-col-content {
padding: 6px 8px;
font-size: 12px;
font-weight: 500;
line-height: 16px;
color: var(--coz-fg-secondary);
}
.col-content {
padding: 12px 8px;
}
}
}
.error-msg-box {
position: relative;
top: -24px;
.error-msg {
display: block;
padding: 8px 16px;
line-height: 16px;
color: #F93920;
text-align: left;
.link {
font-weight: 400;
color: #4D53E8;
}
}
}
.creation-method {
display: flex !important;
flex-direction: column;
gap: 4px;
justify-content: space-between;
padding: 0 !important;
:global {
.semi-radio {
padding: 8px 12px;
background-color: var(--coz-mg-card);
border: solid 1px var(--coz-stroke-plus);
border-radius: 8px;
&:hover {
background-color: var(--coz-mg-secondary-hovered);
}
&:active {
background-color: var(--coz-mg-secondary-pressed);
}
}
.semi-radio-inner {
display: none;
}
.semi-radio-addon {
line-height: 20px;
}
.semi-radio-checked {
background: var(--coz-mg-hglt);
border: 1px solid var(--coz-stroke-hglt);
&:hover {
background-color: var(--coz-mg-hglt-hovered);
}
&:active {
background-color: var(--coz-mg-hglt-pressed);
}
}
}
}
.code-runtime-list {
:global {
.semi-select-option-selected .semi-select-option-icon {
color: #4d53e8;
}
}
}
.bot-code-edit-title-action {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}

View File

@@ -0,0 +1,25 @@
/*
* 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 { CreateFormPluginModal } from './bot-form-edit';
export { CreateCodePluginModal } from './bot-code-edit';
export {
useBotCodeEditInPlugin,
useBotFormEditInPlugin,
useImportToolInPlugin,
useBotCodeEditOutPlugin,
} from './plugin-edit';

View File

@@ -0,0 +1,3 @@
.actions {
margin-right: 20px;
}

View File

@@ -0,0 +1,268 @@
/*
* 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, useEffect, useMemo, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { I18n } from '@coze-arch/i18n';
import { UIButton } from '@coze-arch/bot-semi';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import { type PluginInfoProps } from '@coze-studio/plugin-shared';
import {
checkOutPluginContext,
unlockOutPluginContext,
usePluginStore,
} from '@coze-studio/bot-plugin-store';
import {
CreateFormPluginModal,
type CreatePluginFormProps,
} from '../bot-form-edit';
import {
CreateCodePluginModal,
type CreatePluginProps,
} from '../bot-code-edit';
import { ImportToolModal, type ImportToolModalProps } from '../../file-import';
import styles from './index.module.less';
export const useBotCodeEditInPlugin = ({
modalProps,
}: {
modalProps: Pick<CreatePluginProps, 'onSuccess'>;
}) => {
const { pluginInfo, canEdit, unlockPlugin, wrapWithCheckLock } =
usePluginStore(
useShallow(store => ({
pluginInfo: store.pluginInfo,
canEdit: store.canEdit,
unlockPlugin: store.unlockPlugin,
wrapWithCheckLock: store.wrapWithCheckLock,
})),
);
const [showCodePluginModel, setShowCodePluginModel] = useState(false);
const [editable, setEditable] = useState(false);
const action = useMemo(() => {
if (!canEdit) {
return null;
}
return (
<div className={styles.actions}>
{editable ? (
<UIButton
onClick={() => {
setEditable(false);
unlockPlugin();
}}
>
{I18n.t('Cancel')}
</UIButton>
) : (
<UIButton
theme="solid"
onClick={wrapWithCheckLock(() => setEditable(true))}
>
{I18n.t('Edit')}
</UIButton>
)}
</div>
);
}, [editable, canEdit]);
useEffect(() => {
if (showCodePluginModel) {
setEditable(false);
}
}, [showCodePluginModel]);
const modal = (
<CreateCodePluginModal
{...modalProps}
isCreate={false}
visible={showCodePluginModel}
onCancel={() => {
setShowCodePluginModel(false);
unlockPlugin();
}}
disabled={!editable || !canEdit}
editInfo={pluginInfo}
actions={action}
/>
);
return { modal, setShowCodePluginModel };
};
export const useBotCodeEditOutPlugin = ({
modalProps,
}: {
modalProps: Pick<CreatePluginProps, 'onSuccess'>;
}) => {
const [pluginInfo, setPluginInfo] = useState<PluginInfoProps>({});
const [modalVisible, setModalVisible] = useState(false);
const [editable, setEditable] = useState(false);
const [disableEdit, setDisableEdit] = useState(false);
const pluginId = pluginInfo?.plugin_id || '';
const action = useMemo(() => {
if (disableEdit) {
return null;
}
return (
<div className={styles.actions}>
{editable ? (
<UIButton
onClick={() => {
setEditable(false);
unlockOutPluginContext(pluginId);
}}
>
{I18n.t('Cancel')}
</UIButton>
) : (
<UIButton
theme="solid"
onClick={async () => {
const isLocked = await checkOutPluginContext(pluginId);
if (isLocked) {
return;
}
setEditable(true);
}}
>
{I18n.t('Edit')}
</UIButton>
)}
</div>
);
}, [editable, pluginId, disableEdit]);
useEffect(() => {
if (modalVisible) {
setEditable(false);
}
}, [modalVisible]);
const modal = (
<CreateCodePluginModal
{...modalProps}
editInfo={pluginInfo}
isCreate={false}
visible={modalVisible}
onCancel={() => {
setModalVisible(false);
if (!disableEdit) {
unlockOutPluginContext(pluginId);
}
}}
disabled={!editable}
actions={action}
/>
);
const open = useCallback(async (id: string, disable: boolean) => {
const res = await PluginDevelopApi.GetPluginInfo({
plugin_id: id || '',
});
setPluginInfo({
plugin_id: id,
code_info: {
plugin_desc: res.code_info?.plugin_desc,
/** yaml */
openapi_desc: res.code_info?.openapi_desc,
client_id: res.code_info?.client_id,
client_secret: res.code_info?.client_secret,
service_token: res.code_info?.service_token,
},
});
setDisableEdit(disable);
setModalVisible(true);
}, []);
return { modal, open };
};
export const useBotFormEditInPlugin = ({
modalProps,
}: {
modalProps: Pick<CreatePluginFormProps, 'onSuccess'>;
}) => {
const { pluginInfo, canEdit, unlockPlugin } = usePluginStore(store => ({
pluginInfo: store.pluginInfo,
canEdit: store.canEdit,
unlockPlugin: store.unlockPlugin,
}));
const [showFormPluginModel, setShowFormPluginModel] = useState(false);
const modal = (
<CreateFormPluginModal
{...modalProps}
isCreate={false}
visible={showFormPluginModel}
editInfo={pluginInfo}
onCancel={() => {
unlockPlugin();
setShowFormPluginModel(false);
}}
disabled={!canEdit}
/>
);
return {
modal,
setShowFormPluginModel,
};
};
export const useImportToolInPlugin = ({
modalProps,
}: {
modalProps: Pick<ImportToolModalProps, 'onSuccess'>;
}) => {
const { pluginInfo, unlockPlugin } = usePluginStore(store => ({
pluginInfo: store.pluginInfo,
unlockPlugin: store.unlockPlugin,
}));
const [showImportToolModal, setShowImportToolModal] = useState(false);
const modal = (
<ImportToolModal
{...modalProps}
pluginInfo={{
pluginID: pluginInfo?.plugin_id,
pluginName: pluginInfo?.meta_info?.name,
pluginUrl: pluginInfo?.meta_info?.url,
pluginDesc: pluginInfo?.meta_info?.desc,
editVersion: pluginInfo?.edit_version,
}}
visible={showImportToolModal}
onCancel={() => {
unlockPlugin();
setShowImportToolModal(false);
}}
/>
);
return {
modal,
setShowImportToolModal,
};
};

View File

@@ -0,0 +1,58 @@
/*
* 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 { Editor as MonacoEditor } from '@coze-arch/bot-monaco-editor';
interface EditorPros {
mode: 'yaml' | 'json' | 'javascript';
value?: string;
onChange?: (v: string | undefined) => void;
height?: number | string;
useValidate?: boolean;
theme?: string;
disabled?: boolean;
dataTestID?: string;
}
export const Editor: React.FC<EditorPros> = ({
mode,
value,
onChange,
height = 500,
theme = 'monokai',
disabled = false,
dataTestID,
}) => {
const [heightVal, setHeightVal] = useState(height);
useEffect(() => {
setHeightVal(height);
}, [height]);
return (
<div style={{ position: 'relative' }} data-testid={dataTestID}>
<MonacoEditor
options={{ readOnly: disabled }}
language={mode}
theme={theme}
width="100%"
onChange={onChange}
height={heightVal}
value={value}
/>
</div>
);
};

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 { Editor } from './editor';

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export function getEnv(): string {
if (!IS_PROD) {
return 'cn-boe';
}
const regionPart = IS_OVERSEA ? 'oversea' : 'cn';
const inhousePart = IS_RELEASE_VERSION ? 'release' : 'inhouse';
return [regionPart, inhousePart].join('-');
}
// error code
export const ERROR_CODE = {
SAFE_CHECK: 720092020,
DUP_NAME_URL: 702093022,
DUP_NAME: 702092010,
DUP_PATH: 702093021,
};
export const ACCEPT_FORMAT = ['json', 'yaml'];
export const ACCEPT_EXT = ACCEPT_FORMAT.map(item => `.${item}`);
export const INITIAL_PLUGIN_REPORT_PARAMS = {
environment: getEnv(),
workspace_id: '',
workspace_type: '',
status: 1,
create_type: 'import',
};
export const INITIAL_TOOL_REPORT_PARAMS = {
environment: getEnv(),
workspace_id: '',
workspace_type: '',
status: 1,
create_type: 'import',
plugin_id: '',
};

View File

@@ -0,0 +1,134 @@
/* stylelint-disable order/order */
.upload-file-area {
:global {
.semi-upload-file-list {
width: 100%;
margin-top: 0;
margin-bottom: 0;
.semi-upload-file-list-main {
margin-bottom: 0;
}
}
.semi-upload-drag-area {
height: 380px;
background-color: white;
border: 1px dashed var(--semi-color-border);
}
.semi-upload-drag-area-tips {
font-weight: 400;
}
.semi-upload-drag-area-legal {
border: 1px dashed var(--semi-color-primary);
}
}
a {
color: #4D53E8;
text-decoration: none;
}
}
.drag-area-disabled {
:global {
.semi-upload-drag-area {
display: none;
}
}
}
.upload-file-item {
display: flex;
align-items: center;
width: 100%;
padding: 16px 20px;
background: #FFF;
border: 1px solid #1D1C2314;
border-radius: 8px;
.file-icon {
display: flex;
align-items: center;
width: 24px;
height: 24px;
margin-right: 16px;
font-size: 24px;
img {
width: 24px;
height: 24px;
}
}
.text {
flex: 1;
margin-right: 16px;
}
.upload-text {
color: #1D1C2359;
}
.progress {
display: flex;
align-items: center;
width: 160px;
padding: 0 16px;
}
.delete-icon {
font-size: 16px;
}
}
.text-area {
background-color: #FFF;
&:hover {
background-color: #FFF;
}
:global {
.semi-input-textarea {
&::-webkit-scrollbar {
width: 8px;
height: 4px;
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(29 28 35 / 30%);
border-radius: 6px;
&:hover {
background: rgb(29 28 35 / 60%);
}
}
&::-webkit-scrollbar-corner {
display: none;
}
}
}
}
.disabled {
opacity: 0.5;
}

View File

@@ -0,0 +1,219 @@
/*
* 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, { forwardRef, useState } from 'react';
import classNames from 'classnames';
import { logger } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import {
type RenderFileItemProps,
type FileItem,
type UploadProps,
} from '@coze-arch/bot-semi/Upload';
import { type TextAreaProps } from '@coze-arch/bot-semi/Input';
import {
Progress,
TextArea,
Typography,
UIButton,
UIIconButton,
UIToast,
Upload,
Image,
} from '@coze-arch/bot-semi';
import { IconDeleteOutline, IconError } from '@coze-arch/bot-icons';
import YAMLImg from '@/assets/yaml.png';
import JsonImg from '@/assets/json-file.png';
import { getContent, getFileExtension } from './utils';
import { ACCEPT_EXT, ACCEPT_FORMAT } from './const';
import styles from './import-content.module.less';
export interface FileUploadProps {
onUpload: (content?: string) => void;
disabled?: boolean;
}
type SemiTextAreaProps = Omit<TextAreaProps, 'forwardRef'>;
interface RawTextProps extends SemiTextAreaProps {
onChange: (val?: string) => void;
}
export const FileUpload = ({ onUpload, disabled }: FileUploadProps) => {
const [fileList, setFileList] = useState<FileItem[]>([]);
const customRequest: UploadProps['customRequest'] = async options => {
const { onSuccess, file, onError, onProgress } = options;
if (typeof file === 'string') {
return;
}
try {
const { name, fileInstance } = file;
if (fileInstance) {
const extension = getFileExtension(name);
if (!ACCEPT_FORMAT.includes(extension)) {
return;
}
const result = await getContent(fileInstance, onProgress);
onSuccess(result);
}
} catch (error) {
logger.error({
eventName: 'fail_to_read_file',
// @ts-expect-error -- linter-disable-autofix
error,
});
onError({ status: 0 });
}
};
const renderFileItem = (renderFileItemProps: RenderFileItemProps) => {
const { name, onRemove, onRetry, percent, status } = renderFileItemProps;
const renderProgress = () => {
switch (status) {
case 'success':
return (
<Typography.Text className={styles['upload-text']} ellipsis>
{I18n.t('file_upload_success')}
</Typography.Text>
);
case 'uploadFail':
case 'validateFail':
return (
<>
<IconError />
<UIButton
theme="borderless"
className="ml-[8px]"
onClick={onRetry}
>
{I18n.t('retry')}
</UIButton>
</>
);
case 'uploading':
case 'wait':
case 'validating':
default:
return (
<div className={classNames('w-[90px]')}>
<Progress percent={percent} />
</div>
);
}
};
return (
<div
className={classNames(
styles['upload-file-item'],
disabled && styles.disabled,
)}
>
<Image
preview={false}
className={styles['file-icon']}
src={getFileExtension(name) === 'yaml' ? YAMLImg : JsonImg}
/>
<Typography.Text
className={styles.text}
ellipsis={{ showTooltip: { opts: { content: name } } }}
>
{name}
</Typography.Text>
{<div className={styles.progress}>{renderProgress()}</div>}
<UIIconButton
icon={
<IconDeleteOutline
className={styles['delete-icon']}
onClick={onRemove}
/>
}
/>
</div>
);
};
return (
<Upload
accept={ACCEPT_EXT.join(',')}
action=""
onAcceptInvalid={() => {
UIToast.warning(I18n.t('file_format_not_supported'));
}}
onSuccess={res => {
onUpload(res);
}}
disabled={disabled}
fileList={fileList}
onChange={({ fileList: list }) => {
setFileList(list);
if (!list.length) {
// 清空content
onUpload();
}
}}
className={classNames(
styles['upload-file-area'],
fileList.length && styles['drag-area-disabled'],
)}
dragMainText={I18n.t('click_upload_or_drag_files')}
draggable={true}
dragSubText={
<>
<span>{I18n.t('supports_uploading_json_or_yaml_files')}</span>
<a
href={
IS_OVERSEA
? '/open/docs/guides/plugin_import'
: '/open/docs/guides/import'
}
target="_blank"
onClick={e => e.stopPropagation()}
>
{I18n.t('view_detailed_information')}
</a>
</>
}
renderFileItem={renderFileItem}
limit={1}
customRequest={customRequest}
/>
);
};
export const RawText = forwardRef<HTMLTextAreaElement | null, RawTextProps>(
(props, ref) => {
const { onChange, ...extraProps } = props;
return (
<TextArea
placeholder={I18n.t('enter_raw_content_or_url')}
rows={17}
{...extraProps}
ref={ref}
onChange={value => {
onChange(value.trim());
}}
className={styles['text-area']}
/>
);
},
);

View File

@@ -0,0 +1,38 @@
.radio-group {
:global {
.semi-radio {
width: 136px;
}
.semi-radio-content {
width: 128px;
}
/* stylelint-disable-next-line selector-class-pattern */
.semi-radio-addon-buttonRadio {
border-radius: 4px;
}
}
}
.error-msg {
width: 100%;
height: 22px;
margin-top: 8px;
:global {
.semi-typography {
color: #FF5656;
}
}
}
.import-modal {
:global {
.semi-modal-content {
min-height: 646px;
}
}
}

View File

@@ -0,0 +1,194 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState, useEffect, useRef } from 'react';
import { logger } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import {
UIButton,
UIModal,
RadioGroup,
Radio,
Typography,
} from '@coze-arch/bot-semi';
import { isValidURL, customService } from './utils';
import { FileUpload, RawText } from './import-content';
import styles from './import-modal.module.less';
export enum ImportType {
File = 'File',
Text = 'Text',
}
enum ImportDetailType {
File = 'file',
FileUrl = 'file_url',
Text = 'raw_txt',
}
export interface ImportData {
type?: ImportDetailType;
content?: string;
}
export interface ImportModalProps {
visible: boolean;
title?: React.ReactNode;
onCancel?: () => void;
onOk?: (
data: ImportData,
) => Promise<{ success?: boolean; result?: unknown; errMsg?: string }>;
}
export const ImportModal: React.FC<ImportModalProps> = props => {
const { onCancel, visible, onOk, title } = props;
const [importType, setImportType] = useState(ImportType.File);
const [content, setContent] = useState<string>();
const [errMsg, setErrMsg] = useState<string>();
const [loading, setLoading] = useState(false);
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
const handleContent = (text?: string) => {
setContent(text);
};
const reset = () => {
setImportType(ImportType.File);
setContent(undefined);
};
const handleParse = async () => {
setLoading(true);
setErrMsg(undefined);
let originContent = content;
let type: ImportDetailType =
importType === ImportType.Text
? ImportDetailType.Text
: ImportDetailType.File;
if (importType === ImportType.Text && isValidURL(content)) {
try {
const res = await customService(content || '');
originContent = res as unknown as string;
type = ImportDetailType.FileUrl;
} catch (e) {
// @ts-expect-error -- linter-disable-autofix
logger.error({ error: e, eventName: 'fetch_url_resource_fail' });
setErrMsg(I18n.t('unable_to_access_input_url'));
setLoading(false);
return Promise.reject(e);
}
}
try {
const res = await onOk?.({ type, content: originContent });
if (!res?.success) {
setErrMsg(res?.errMsg);
}
} finally {
setLoading(false);
}
};
const renderFooter = () => (
<UIButton
theme="solid"
type="primary"
disabled={!content}
onClick={handleParse}
loading={loading}
>
{I18n.t('next')}
</UIButton>
);
const renderErrMsg = () =>
errMsg ? (
<Typography.Text
ellipsis={{
showTooltip: {
opts: { content: errMsg },
},
}}
>
{errMsg}
</Typography.Text>
) : null;
useEffect(() => {
errMsg && setErrMsg(undefined);
}, [content]);
useEffect(() => {
if (visible) {
reset();
}
}, [visible]);
useEffect(() => {
if (importType === ImportType.Text && textAreaRef.current) {
textAreaRef?.current?.focus();
}
}, [importType]);
return (
<>
<UIModal
afterClose={reset}
keepDOM={false}
type="action-small"
title={title}
visible={visible}
onCancel={onCancel}
onOk={handleParse}
footer={renderFooter()}
className={styles['import-modal']}
>
<div className="min-h-[472px]">
<div className="flex justify-center mb-[24px]">
<RadioGroup
onChange={e => {
setImportType(e.target.value);
setContent(undefined);
}}
type="button"
buttonSize="middle"
defaultValue={importType}
disabled={loading}
className={styles['radio-group']}
>
<Radio value={ImportType.File}>{I18n.t('local_file')}</Radio>
<Radio value={ImportType.Text}>{I18n.t('url_raw_data')}</Radio>
</RadioGroup>
</div>
<div>
{importType === ImportType.File ? (
<FileUpload onUpload={handleContent} disabled={loading} />
) : (
<RawText
onChange={handleContent}
disabled={loading}
ref={textAreaRef}
/>
)}
</div>
<div className={styles['error-msg']}> {renderErrMsg()}</div>
</div>
</UIModal>
</>
);
};

View File

@@ -0,0 +1,474 @@
/*
* 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, useRef, useState } from 'react';
import { type AxiosResponse } from 'axios';
import { userStoreService } from '@coze-studio/user-store';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { logger } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import {
EVENT_NAMES,
sendTeaEvent,
type ParamsTypeDefine,
} from '@coze-arch/bot-tea';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { UIToast, UIModal } from '@coze-arch/bot-semi';
import { IconWarningInfo } from '@coze-arch/bot-icons';
import { type ApiError } from '@coze-arch/bot-http';
import {
type Convert2OpenAPIRequest,
type BatchCreateAPIRequest,
type BatchCreateAPIResponse,
type Convert2OpenAPIResponse,
} from '@coze-arch/bot-api/plugin_develop';
import {
SpaceType,
type PluginMetaInfo,
} from '@coze-arch/bot-api/developer_api';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import {
getImportFormatType,
getInitialPluginMetaInfo,
isDuplicatePathErrorResponseData,
parsePluginInfo,
} from './utils';
import { showMergeTool } from './show-merge-tool';
import { PluginInfoConfirm } from './plugin-info-confirm';
import { type ImportData, ImportModal } from './import-modal';
import {
ERROR_CODE,
INITIAL_PLUGIN_REPORT_PARAMS,
INITIAL_TOOL_REPORT_PARAMS,
getEnv,
} from './const';
interface ImportModalProps {
visible: boolean;
onCancel?: () => void;
onSuccess?: (pluginInfo?: { plugin_id?: string }) => void;
projectId?: string;
}
export type ImportPluginModalProps = ImportModalProps;
export interface ImportToolModalProps extends ImportModalProps {
pluginInfo?: {
pluginName?: string;
pluginUrl?: string;
pluginID?: string;
pluginDesc?: string;
editVersion?: number;
};
}
interface ImportPluginInfo {
aiPlugin?: string;
openAPI?: string;
metaInfo?: PluginMetaInfo;
}
export const ImportPluginModal: React.FC<ImportPluginModalProps> = props => {
const { visible } = props;
return visible ? <ImportPluginModalContent {...props} /> : null;
};
export const ImportPluginModalContent: React.FC<
ImportPluginModalProps
> = props => {
const { onCancel, visible, onSuccess, projectId } = props;
const [importPluginInfo, setImportPluginInfo] = useState<ImportPluginInfo>();
const { id: spaceId, space_type } = useSpaceStore(store => store.space);
const reportParams = useRef<
ParamsTypeDefine[EVENT_NAMES.create_plugin_front]
>(INITIAL_PLUGIN_REPORT_PARAMS);
const isPersonal = space_type === SpaceType.Personal;
useEffect(() => {
reportParams.current = {
...reportParams.current,
environment: getEnv(),
workspace_id: spaceId || '',
workspace_type: isPersonal ? 'personal_workspace' : 'team_workspace',
status: 1,
create_type: 'import',
};
}, [spaceId, isPersonal]);
const convert2OpenAPI = async (
req: Convert2OpenAPIRequest,
): Promise<{ success: boolean; errMsg?: string }> => {
try {
const { openapi, ai_plugin, plugin_data_format } =
await PluginDevelopApi.Convert2OpenAPI(req, {
__disableErrorToast: true,
});
// 解析string
const result = parsePluginInfo({
aiPlugin: ai_plugin,
openAPI: openapi,
});
const metaImportPluginInfo = getInitialPluginMetaInfo(result);
setImportPluginInfo({
aiPlugin: ai_plugin,
openAPI: openapi,
metaInfo: metaImportPluginInfo,
});
reportParams.current = {
...reportParams.current,
import_format_type: getImportFormatType(plugin_data_format),
import_tools_count: Object.entries(result?.openAPI?.paths || {}).length,
};
return {
success: true,
};
} catch (e) {
const { msg, code, response } = e as ApiError;
// @ts-expect-error -- linter-disable-autofix
logger.error({ error: e, eventName: 'plugin_convert_openapi_fail' });
reportParams.current = {
...reportParams.current,
import_format_type: getImportFormatType(
(response as unknown as AxiosResponse<Convert2OpenAPIResponse>)?.data
?.plugin_data_format,
),
import_tools_count: 0,
};
sendTeaEvent(EVENT_NAMES.create_plugin_front, {
...reportParams.current,
status: 1,
error_message: msg,
});
if (
Number(code) === ERROR_CODE.DUP_PATH ||
isDuplicatePathErrorResponseData(response?.data)
) {
const handleMerge = async () => {
const { errMsg, success } = await convert2OpenAPI({
...req,
merge_same_paths: true,
});
if (!success) {
UIToast.error({
content: withSlardarIdButton(errMsg || I18n.t('error')),
});
return Promise.reject(errMsg);
}
};
showMergeTool({
onOk: handleMerge,
duplicateInfos: (
response as unknown as AxiosResponse<Convert2OpenAPIResponse>
)?.data?.duplicate_api_infos,
});
return { success: false, errMsg: '' };
} else {
return {
success: false,
errMsg: msg || I18n.t('error'),
};
}
}
};
const handleConvertOpenAPI = async ({ content, type }: ImportData) => {
reportParams.current = {
...reportParams.current,
import_way_type: type,
};
return await convert2OpenAPI({
data: content || '',
space_id: spaceId,
merge_same_paths: false,
});
};
return (
<>
<ImportModal
title={I18n.t('import_plugin')}
visible={visible}
onCancel={onCancel}
onOk={handleConvertOpenAPI}
/>
{importPluginInfo ? (
<PluginInfoConfirm
visible={!!importPluginInfo}
projectId={projectId}
onCancel={() => setImportPluginInfo(undefined)}
importInfo={{
metaInfo: importPluginInfo.metaInfo,
openAPI: importPluginInfo.openAPI,
aiPlugin: importPluginInfo.aiPlugin,
extra: { reportParams: reportParams.current },
}}
onSuccess={data => {
onCancel?.();
onSuccess?.(data);
}}
/>
) : null}
</>
);
};
export const ImportToolModal: React.FC<ImportToolModalProps> = props => {
const { visible } = props;
return visible ? <ImportToolModalContent {...props} /> : null;
};
export const ImportToolModalContent: React.FC<ImportToolModalProps> = props => {
const { onCancel, visible, onSuccess, pluginInfo } = props;
const reportParams = useRef<
ParamsTypeDefine[EVENT_NAMES.create_plugin_tool_front]
>(INITIAL_TOOL_REPORT_PARAMS);
const { id: spaceId, space_type } = useSpaceStore(store => store.space);
const isPersonal = space_type === SpaceType.Personal;
const userInfo = userStoreService.useUserInfo();
useEffect(() => {
reportParams.current = {
...reportParams.current,
environment: getEnv(),
workspace_id: spaceId || '',
workspace_type: isPersonal ? 'personal_workspace' : 'team_workspace',
status: 1,
create_type: 'import',
plugin_id: pluginInfo?.pluginID || '',
};
}, [pluginInfo?.pluginID, spaceId, isPersonal]);
const handleBatchImportTool = async (req?: BatchCreateAPIRequest) => {
try {
const resp = await PluginDevelopApi.BatchCreateAPI(req, {
__disableErrorToast: true,
});
const toolsCount = req?.replace_same_paths
? req?.paths_to_replace?.length
: resp?.paths_created?.length;
sendTeaEvent(EVENT_NAMES.create_plugin_tool_front, {
...reportParams.current,
status: 0,
import_tools_count: toolsCount || 0,
});
if (resp && !resp?.paths_duplicated?.length) {
UIToast.success(
req?.replace_same_paths
? I18n.t('plugin_tool_replace_success')
: I18n.t('plugin_tool_import_succes'),
);
onCancel?.();
onSuccess?.();
}
} catch (e) {
const { code, response } = e as ApiError;
if (
Number(code) !== ERROR_CODE.DUP_PATH &&
!isDuplicatePathErrorResponseData(response?.data)
) {
return Promise.reject(e);
}
sendTeaEvent(EVENT_NAMES.create_plugin_tool_front, {
...reportParams.current,
status: 0,
import_tools_count:
(response as unknown as AxiosResponse<BatchCreateAPIResponse>)?.data
?.paths_created?.length || 0,
});
handleDupPath(
req,
(response as unknown as AxiosResponse<BatchCreateAPIResponse>)?.data,
);
}
};
const handleDupPath = (
req?: BatchCreateAPIRequest,
resp?: BatchCreateAPIResponse,
) => {
const { paths_created = [], paths_duplicated = [] } = resp || {};
const importedLength = paths_created.length;
const duplicatedLength = paths_duplicated.length;
const failedContent = I18n.t('failed_to_import_tool', {
num: duplicatedLength,
});
const successContent = I18n.t('tools_imported_successfully', {
num: importedLength,
});
UIModal.warning({
title: importedLength
? `${successContent}, ${failedContent}`
: failedContent,
content: duplicatedLength
? I18n.t('plugin_tool_exists_tips', { num: duplicatedLength })
: null,
okText: I18n.t('replace'),
cancelText: I18n.t('Cancel'),
centered: true,
icon: <IconWarningInfo />,
okButtonProps: {
type: 'warning',
},
onOk: async () => {
const batchCreateReq: BatchCreateAPIRequest = {
...req,
replace_same_paths: true,
paths_to_replace: paths_duplicated,
};
try {
await handleBatchImportTool(batchCreateReq);
} catch (err) {
const { msg: errMsg } = err as ApiError;
UIToast.error({
content: withSlardarIdButton(errMsg || I18n.t('error')),
});
sendTeaEvent(EVENT_NAMES.create_plugin_tool_front, {
...reportParams.current,
import_tools_count: 0,
status: 1,
error_message: errMsg || '',
});
}
},
onCancel: importedLength
? async () => {
onCancel?.();
await onSuccess?.();
}
: undefined,
});
};
const convertOpenAPI = async (req: Convert2OpenAPIRequest) =>
await PluginDevelopApi.Convert2OpenAPI(
{ ...req, plugin_description: pluginInfo?.pluginDesc },
{
__disableErrorToast: true,
},
);
const batchImport = async (req: Convert2OpenAPIRequest) => {
try {
const resp = await convertOpenAPI(req);
reportParams.current = {
...reportParams.current,
import_format_type: getImportFormatType(resp?.plugin_data_format),
};
const batchCreateReq: BatchCreateAPIRequest = {
plugin_id: pluginInfo?.pluginID,
ai_plugin: resp?.ai_plugin,
openapi: resp?.openapi,
replace_same_paths: false,
space_id: spaceId,
dev_id: userInfo?.user_id_str,
edit_version: pluginInfo?.editVersion,
};
await handleBatchImportTool(batchCreateReq);
return { success: true };
} catch (e) {
const { msg, code, response } = e as ApiError;
// @ts-expect-error -- linter-disable-autofix
logger.error({ error: e, eventName: 'batch_create_fail' });
reportParams.current = {
...reportParams.current,
import_format_type: getImportFormatType(
(response as unknown as AxiosResponse<Convert2OpenAPIResponse>)?.data
?.plugin_data_format,
),
};
sendTeaEvent(EVENT_NAMES.create_plugin_tool_front, {
...reportParams.current,
import_tools_count: 0,
status: 1,
error_message: msg || '',
});
if (
Number(code) === ERROR_CODE.DUP_PATH ||
isDuplicatePathErrorResponseData(response?.data)
) {
const handleMerge = async () => {
const { success, errMsg } = await batchImport({
...req,
merge_same_paths: true,
});
if (!success) {
UIToast.error({
content: withSlardarIdButton(errMsg || I18n.t('error')),
});
return Promise.reject(e);
}
};
showMergeTool({
onOk: handleMerge,
duplicateInfos: (
response as unknown as AxiosResponse<Convert2OpenAPIResponse>
)?.data?.duplicate_api_infos,
});
return { success: false };
} else {
return {
success: false,
errMsg: msg || I18n.t('error'),
};
}
}
};
const handleImport = async (importData?: ImportData) => {
const { content, type } = importData || {};
reportParams.current = {
...reportParams.current,
import_way_type: type,
};
const res = await batchImport({
data: content || '',
plugin_name: pluginInfo?.pluginName,
plugin_url: pluginInfo?.pluginUrl,
merge_same_paths: false,
space_id: spaceId,
});
return res;
};
return (
<ImportModal
title={I18n.t('import_plugin_tool')}
visible={visible}
onCancel={onCancel}
onOk={handleImport}
/>
);
};

View File

@@ -0,0 +1,475 @@
/*
* 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 { InputWithCountField } from '@coze-studio/components';
import { I18n } from '@coze-arch/i18n';
import { safeJSONParse } from '@coze-arch/bot-utils';
import {
Col,
Form,
Row,
UICascader,
UIFormInput,
UIFormTextArea,
UIIconButton,
UIInput,
useFormApi,
withField,
} from '@coze-arch/bot-semi';
import { type commonParamSchema } from '@coze-arch/bot-api/developer_api';
import {
type OauthTccOpt,
authOptionsPlaceholder,
extInfoText,
locationOption,
} from '@coze-studio/plugin-shared';
import { IconAdd, IconDeleteOutline } from '@coze-arch/bot-icons';
import { InfoPopover } from '@coze-agent-ide/bot-plugin-tools/infoPopover';
import { type AuthOption, findAuthTypeItem, formRuleList } from './utils';
import { type ConfirmFormProps } from './interface';
import s from './index.module.less';
interface PluginInfoFormFieldProps {
disabled?: boolean;
}
const HEADER_LIST_LENGTH_MAX = 20;
export const PluginNameField = ({ disabled }: PluginInfoFormFieldProps) => {
const formApi = useFormApi<ConfirmFormProps>();
const formValues = formApi.getValues();
return disabled ? (
<Form.Slot
label={{
text: I18n.t('create_plugin_modal_name1'),
required: true,
}}
>
<div>{formValues?.name}</div>
</Form.Slot>
) : (
<UIFormTextArea
field="name"
className={s['textarea-single-line']}
label={I18n.t('create_plugin_modal_name1')}
placeholder={I18n.t('create_plugin_modal_name2')}
trigger={['blur', 'change']}
maxCount={30}
maxLength={30}
rows={1}
onBlur={() => {
formApi.setValue('name', formApi.getValue('name')?.trim());
}}
rules={formRuleList.name}
/>
);
};
export const PluginDescField = ({ disabled }: PluginInfoFormFieldProps) => {
const formApi = useFormApi<ConfirmFormProps>();
const formValues = formApi.getValues();
return disabled ? (
<Form.Slot
label={{
text: I18n.t('create_plugin_modal_descrip1'),
required: true,
}}
>
<div>{formValues?.desc}</div>
</Form.Slot>
) : (
<UIFormTextArea
field="desc"
label={I18n.t('create_plugin_modal_descrip1')}
trigger={['blur', 'change']}
placeholder={I18n.t('create_plugin_modal_descrip2')}
rows={2}
maxCount={600}
maxLength={600}
onBlur={() => {
formApi.setValue('desc', formValues?.desc?.trim());
}}
rules={formRuleList.desc}
/>
);
};
export const PluginUrlField = ({ disabled }: PluginInfoFormFieldProps) => {
const formApi = useFormApi<ConfirmFormProps>();
const formValues = formApi.getValues();
return disabled ? (
<Form.Slot
label={{
text: I18n.t('create_plugin_modal_url1'),
required: true,
}}
>
<div>{formValues?.url}</div>
</Form.Slot>
) : (
<UIFormInput
className={s['textarea-single-line']}
trigger={['blur', 'change']}
field="url"
label={I18n.t('create_plugin_modal_url1')}
placeholder={I18n.t('create_plugin_modal_url2')}
onBlur={() => {
formApi.setValue('url', formValues?.url?.trim());
}}
rules={formRuleList.url}
/>
);
};
const HeaderList = ({
disabled,
value: headerList = [],
onChange: setHeaderList,
}: PluginInfoFormFieldProps & {
value?: commonParamSchema[];
onChange?: (val?: commonParamSchema[]) => void;
}) => {
/** 添加header */
// @ts-expect-error -- linter-disable-autofix
const addHeader = data => {
const h = [...headerList];
h.push(data.name ? data : { name: '', value: '' });
setHeaderList?.(h);
};
/** 删除header */
// @ts-expect-error -- linter-disable-autofix
const deleteHeader = index => {
// 若为最后一个header则只清空内容不删除
const filterList = cloneDeep(headerList);
filterList.splice(index, 1);
setHeaderList?.(filterList);
};
return (
<Form.Slot
className={s['header-list']}
label={{
text: I18n.t('plugin_create_header_list_title'),
align: 'right',
extra: (
<div className={s['header-list-extra']}>
<InfoPopover data={extInfoText.header_list} />
{headerList.length < HEADER_LIST_LENGTH_MAX && !disabled && (
<UIIconButton
size="large"
icon={<IconAdd />}
onClick={addHeader}
/>
)}
</div>
),
}}
>
<div className={s['herder-list-box']}>
<Row className={s['header-row']} gutter={8}>
<Col span={9}>
<div className={s['header-col-content']}>Key</div>
</Col>
<Col span={12}>
<div className={s['header-col-content']}>Value</div>
</Col>
<Col span={3}>
<div
className={s['header-col-content']}
style={{ textAlign: 'right' }}
>
{I18n.t('plugin_create_action_btn')}
</div>
</Col>
</Row>
<div className={s['herder-list-cotent']}>
{headerList?.map((item, index) => (
<Row
gutter={8}
type="flex"
justify="space-between"
align="middle"
key={index}
>
<Col span={9}>
<div className={s['col-content']}>
<UIInput
placeholder={'Name'}
value={item.name}
onChange={val => {
const list = cloneDeep(headerList);
list[index].name = val;
setHeaderList?.(list);
}}
maxLength={100}
disabled={disabled}
/>
</div>
</Col>
<Col span={12}>
<div className={s['col-content']}>
<UIInput
placeholder={'Value'}
value={item.value}
onChange={val => {
const list = cloneDeep(headerList);
list[index].value = val;
setHeaderList?.(list);
}}
maxLength={200}
disabled={disabled}
/>
</div>
</Col>
<Col span={3}>
<div className={s['col-content']}>
<UIIconButton
icon={<IconDeleteOutline />}
type="secondary"
disabled={disabled}
onClick={() => {
deleteHeader(index);
}}
/>
</div>
</Col>
</Row>
))}
</div>
</div>
</Form.Slot>
);
};
const HeaderListInnerField = withField(HeaderList, {
valueKey: 'value',
onKeyChangeFnName: 'onChange',
});
export const HeaderListField = (props: PluginInfoFormFieldProps) => (
<HeaderListInnerField
{...props}
field="headerList"
label={{ text: '' }}
></HeaderListInnerField>
);
export const AuthTypeField = ({
disabled,
authOption,
onChange,
}: PluginInfoFormFieldProps & {
authOption: Array<AuthOption>;
onChange: (val?: Array<number>) => void;
}) => {
const formApi = useFormApi<ConfirmFormProps>();
const formValues = formApi.getValues();
return disabled ? (
<Form.Slot
label={{
text: I18n.t('create_plugin_modal_auth1'),
extra: <InfoPopover data={extInfoText.auth} />,
required: true,
}}
>
<div>
{findAuthTypeItem(authOption, formValues?.auth_type?.at(-1))?.label}
</div>
</Form.Slot>
) : (
<UICascader.FormItem
rules={[{ required: true }]}
style={{ width: '100%' }}
initValue={formValues?.auth_type || [0]}
field="auth_type"
label={{
text: I18n.t('create_plugin_modal_auth1'),
extra: <InfoPopover data={extInfoText.auth} />,
}}
placeholder={I18n.t('please_select_an_authorization_method')}
treeData={authOption}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
displayRender={(list: any) => `${(list as string[])?.at(-1)}`}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onChange={(val: any) => {
onChange(val as Array<number>);
}}
/>
);
};
export const ServiceField = ({ disabled }: PluginInfoFormFieldProps) => {
const formApi = useFormApi<ConfirmFormProps>();
const formValues = formApi.getValues();
return (
<>
{disabled ? (
<Form.Slot
label={{
text: I18n.t('create_plugin_modal_location'),
extra: <InfoPopover data={extInfoText.location} />,
required: true,
}}
>
<div>
{
findAuthTypeItem(locationOption, formApi.getValues()?.location)
?.label
}
</div>
</Form.Slot>
) : (
<Form.RadioGroup
rules={[{ required: true }]}
field="location"
label={{
text: I18n.t('create_plugin_modal_location'),
extra: <InfoPopover data={extInfoText.location} />,
}}
options={locationOption}
/>
)}
{disabled ? (
<Form.Slot
label={{
text: I18n.t('create_plugin_modal_Parameter'),
extra: <InfoPopover data={extInfoText.key} />,
required: true,
}}
>
<div>{formApi.getValues()?.key}</div>
</Form.Slot>
) : (
<InputWithCountField
initValue={formValues?.key}
trigger={['blur', 'change']}
field="key"
label={{
text: I18n.t('create_plugin_modal_Parameter'),
extra: <InfoPopover data={extInfoText.key} />,
}}
placeholder={I18n.t('create_plugin_modal_Parameter_empty')}
maxLength={100}
rules={formRuleList.key}
/>
)}
{disabled ? (
<Form.Slot
label={{
text: I18n.t('create_plugin_modal_Servicetoken'),
extra: <InfoPopover data={extInfoText.service_token} />,
required: true,
}}
>
<div>{formValues?.service_token}</div>
</Form.Slot>
) : (
<InputWithCountField
initValue={formValues?.service_token}
trigger={['blur', 'change']}
field="service_token"
label={{
text: I18n.t('create_plugin_modal_Servicetoken'),
extra: <InfoPopover data={extInfoText.service_token} />,
}}
placeholder={I18n.t('create_plugin_modal_Servicetoken_empty')}
maxLength={400}
rules={formRuleList.service_token}
/>
)}
</>
);
};
// extItems 动态下发
export const ExtItems = ({
disabled,
extItems,
}: PluginInfoFormFieldProps & { extItems: OauthTccOpt[] }) => {
const formApi = useFormApi<ConfirmFormProps>();
const formValues = formApi.getValues();
return (
<>
{/* 服务端动态返回授权项 */}
{extItems?.map(item => (
<>
{disabled ? (
<Form.Slot
key={item.key}
label={{
text: item.key,
extra: extInfoText[item.key] && (
<InfoPopover data={extInfoText[item.key]} />
),
required: item.required,
}}
>
<div>
{formValues?.oauth_info
? safeJSONParse(formValues.oauth_info)[item.key]
: null}
</div>
</Form.Slot>
) : (
<InputWithCountField
key={item.key}
trigger={['blur', 'change']}
field={item.key}
label={{
text: item.key,
extra: extInfoText[item.key] && (
<InfoPopover data={extInfoText[item.key]} />
),
}}
// @ts-expect-error -- linter-disable-autofix
placeholder={authOptionsPlaceholder[item.key]}
initValue={
(formValues?.oauth_info &&
safeJSONParse(formValues.oauth_info)[item.key]) ||
item.default
}
maxLength={item.max_len}
rules={[
{
required: item.required,
// @ts-expect-error -- linter-disable-autofix
message: authOptionsPlaceholder[item.key],
},
item.type === 'url'
? {
pattern: /^(http|https):\/\/.+$/,
message: I18n.t('create_plugin_modal_URLerror'),
}
: {
// eslint-disable-next-line no-control-regex -- regex
pattern: /^[\x00-\x7F]+$/,
message: I18n.t('create_plugin_modal_descrip_error'),
},
]}
/>
)}
</>
))}
</>
);
};

View File

@@ -0,0 +1,152 @@
/* stylelint-disable declaration-no-important */
.upload-form {
.upload-field {
padding-top: 0;
:global {
.semi-form-field-help-text {
justify-content: center;
}
}
}
.textarea-single-line {
:global {
.semi-input-textarea-counter {
position: absolute;
top: 6px;
right: 0;
}
}
}
.textarea-multi-line {
margin-bottom: 16px;
:global {
.semi-input-textarea-counter {
position: absolute;
right: 0;
bottom: -20px;
min-height: 0;
padding: 0;
}
}
}
.footer-draft {
align-items: flex-start;
padding-top: 16px;
font-size: 14px;
line-height: 22px;
color: #000;
.link {
font-weight: 400;
color: #4D53E8;
}
:global {
.semi-icon {
margin-top: 2px;
}
}
}
:global {
input::-webkit-contacts-auto-fill-button {
pointer-events: none;
position: absolute;
right: 0;
display: none !important;
visibility: hidden;
}
}
}
.upload-avatar {
flex-shrink: 0;
width: 80px !important;
height: 80px !important;
background: #fff !important;
border-radius: var(--spacing-tight, 8px) !important;
}
.header-list {
:global {
.semi-form-field-label-with-extra {
padding-right: 0;
}
.semi-form-field-label-extra {
flex: 1;
}
}
.header-list-extra {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.herder-list-box {
overflow: auto;
max-height: 348px;
padding: 0 16px;
border: 1px solid rgb(29 28 35 / 12%);
border-radius: 8px;
.herder-list-cotent {
padding: 12px 0;
}
.header-row {
border-bottom: 1px solid rgb(29 28 35 / 12%);
}
.header-col-content {
padding: 12px 0;
line-height: 16px;
}
.col-content {
padding: 12px 0;
text-align: center;
}
}
}
.error-msg-box {
position: relative;
top: -24px;
.error-msg {
display: block;
padding: 8px 16px;
line-height: 16px;
color: #F93920;
text-align: left;
.link {
font-weight: 400;
color: #4D53E8;
}
}
}

View File

@@ -0,0 +1,334 @@
/*
* 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, useRef, useState } from 'react';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { I18n } from '@coze-arch/i18n';
import {
EVENT_NAMES,
sendTeaEvent,
type ParamsTypeDefine,
} from '@coze-arch/bot-tea';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { type FormApi } from '@coze-arch/bot-semi/Form';
import { UIModal, UIButton, Space, Form, UIToast } from '@coze-arch/bot-semi';
import { IconInfoCircle } from '@coze-arch/bot-icons';
import {
ParameterLocation,
PluginType,
type PluginMetaInfo,
} from '@coze-arch/bot-api/plugin_develop';
import { FileBizType, IconType } from '@coze-arch/bot-api/developer_api';
import { DeveloperApi, PluginDevelopApi } from '@coze-arch/bot-api';
import { PictureUpload } from '@coze-common/biz-components/picture-upload';
import { type OauthTccOpt } from '@coze-studio/plugin-shared';
import { getRegisterInfo } from '../utils';
import { ERROR_CODE, INITIAL_PLUGIN_REPORT_PARAMS } from '../const';
import { PluginDocs } from '../../plugin-docs';
import { type AuthOption, findAuthTypeItem, getAuthOptions } from './utils';
import { type ConfirmFormProps } from './interface';
import {
AuthTypeField,
ExtItems,
HeaderListField,
PluginDescField,
PluginNameField,
PluginUrlField,
ServiceField,
} from './fields';
import s from './index.module.less';
interface PluginInfoConfirmProps {
visible: boolean;
importInfo?: {
metaInfo?: PluginMetaInfo;
aiPlugin?: string;
openAPI?: string;
extra?: {
reportParams?: ParamsTypeDefine[EVENT_NAMES.create_plugin_front];
};
};
disabled?: boolean;
onCancel?: () => void;
onSuccess?: (pluginInfo?: { plugin_id?: string }) => void;
onError?: () => void;
projectId?: string;
}
const INITIAL_FORM_VALUES = {
headerList: [{ name: 'User-Agent', value: 'Coze/1.0' }],
};
/**
文件导入plugin确认信息弹窗目前和普通创建导入很像调用接口不一样
目前感觉这个确认形式不太友好后续不太确定优化形态所以新建单独文件处理以防污染bot-form-edit
*/
// eslint-disable-next-line complexity
export const PluginInfoConfirm: React.FC<PluginInfoConfirmProps> = props => {
const {
onCancel,
importInfo,
visible,
onSuccess,
disabled = false,
projectId,
} = props;
const [authOption, setAuthOption] = useState<AuthOption[]>([]);
// 合规审核结果
const [isValidCheckResult, setIsValidCheckResult] = useState(true);
const [extItems, setExtItems] = useState<OauthTccOpt[]>([]);
const [submitting, setSubmitting] = useState(false);
const header = importInfo?.metaInfo?.common_params?.[4] || [];
const initialFormValues = importInfo
? { ...importInfo?.metaInfo, headerList: header || [] }
: INITIAL_FORM_VALUES;
const formApi = useRef<FormApi<ConfirmFormProps>>();
const formStateValues = formApi.current?.getFormState()?.values;
const spaceId = useSpaceStore(store => store.space.id);
useEffect(() => {
(async () => {
const res = await DeveloperApi.GetOAuthSchema();
const authOptions = getAuthOptions(res?.oauth_schema);
setAuthOption(authOptions);
})();
}, []);
useEffect(() => {
if (importInfo) {
//更新插件
setExtItems(
findAuthTypeItem(
authOption,
importInfo.metaInfo?.auth_type?.at(-1) || 0,
)?.items || [],
);
} else {
setExtItems([]);
}
}, [authOption, importInfo]);
// eslint-disable-next-line complexity
const confirmBtn = async () => {
await formApi.current?.validate();
const formValues = formApi.current?.getValues();
if (!formValues) {
return;
}
const { openAPI, aiPlugin } = getEditRegisterInfo(formValues);
try {
setSubmitting(true);
const { data } = await PluginDevelopApi.RegisterPlugin(
{
ai_plugin: aiPlugin,
openapi: openAPI,
plugin_type: PluginType.PLUGIN,
client_id: formValues?.client_id,
client_secret: formValues?.client_secret,
service_token: formValues?.service_token,
import_from_file: true,
space_id: spaceId,
project_id: projectId,
},
{
__disableErrorToast: true,
},
);
UIToast.success(I18n.t('plugin_imported_successfully'));
onCancel?.();
await onSuccess?.({ plugin_id: data?.plugin_id });
sendTeaEvent(EVENT_NAMES.create_plugin_front, {
...(importInfo?.extra?.reportParams || INITIAL_PLUGIN_REPORT_PARAMS),
status: 0,
});
} catch (error) {
// @ts-expect-error -- linter-disable-autofix
const { code, msg } = error;
sendTeaEvent(EVENT_NAMES.create_plugin_front, {
...(importInfo?.extra?.reportParams || INITIAL_PLUGIN_REPORT_PARAMS),
status: 1,
error_message: msg,
});
if (Number(code) === ERROR_CODE.SAFE_CHECK) {
setIsValidCheckResult(false);
} else {
UIToast.error({
content: withSlardarIdButton(msg),
});
}
} finally {
setSubmitting(false);
}
};
const getEditRegisterInfo = (formValues: ConfirmFormProps) => {
const { headerList, plugin_uri = [], ...extraValues } = formValues;
const json: Record<string, string> = {};
extItems?.forEach(item => {
if (item.key in formValues) {
// @ts-expect-error -- linter-disable-autofix
json[item.key] = formValues[item.key];
}
});
const metaParams: PluginMetaInfo = {
...extraValues,
oauth_info: JSON.stringify(json),
icon: { uri: plugin_uri[0]?.uid },
common_params: {
[ParameterLocation.Header]: formValues?.headerList || [],
[ParameterLocation.Body]: [],
[ParameterLocation.Path]: [],
[ParameterLocation.Query]: [],
},
};
const params = getRegisterInfo(metaParams, {
openAPI: importInfo?.openAPI,
aiPlugin: importInfo?.aiPlugin,
});
return params;
};
useEffect(() => {
if (!isValidCheckResult) {
setIsValidCheckResult(true);
}
}, [formStateValues?.name || formStateValues?.desc]);
return (
<>
{visible ? (
<UIModal
type="action-small"
title={I18n.t('confirm_plugin_information')}
visible={visible}
onCancel={() => onCancel?.()}
footer={
!disabled && (
<div>
{!isValidCheckResult && (
<div className={s['error-msg-box']}>
<span className={s['error-msg']}>
{I18n.t('plugin_create_modal_safe_error')}
</span>
</div>
)}
<div>
<UIButton
type="tertiary"
onClick={() => {
onCancel?.();
}}
>
{I18n.t('create_plugin_modal_button_cancel')}
</UIButton>
<UIButton
type="primary"
theme="solid"
loading={submitting}
onClick={() => {
confirmBtn();
}}
>
{I18n.t('create_plugin_modal_button_confirm')}
</UIButton>
</div>
</div>
)
}
>
<Form<typeof initialFormValues>
// @ts-expect-error -- linter-disable-autofix
getFormApi={api => (formApi.current = api)}
showValidateIcon={false}
initValues={{ ...(initialFormValues || {}) }}
className={s['upload-form']}
>
{({ values }) => (
<>
{/* 插件头像 */}
<PictureUpload
noLabel
disabled={disabled}
fieldClassName={s['upload-field']}
field="plugin_uri"
iconType={IconType.Plugin}
fileBizType={FileBizType.BIZ_PLUGIN_ICON}
/>
{/* 插件名称/插件描述/插件URL */}
<PluginNameField disabled={disabled} />
<PluginDescField disabled={disabled} />
<PluginUrlField disabled={true} />
{/* 插件Header */}
<HeaderListField disabled={disabled} />
{/* 授权方式 */}
<AuthTypeField
disabled={disabled}
authOption={authOption}
onChange={val => {
setExtItems(
findAuthTypeItem(authOption, val?.at(-1))?.items || [],
);
}}
/>
{/* 授权方式-Service */}
{values.auth_type.at(-1) === 1 && (
<ServiceField disabled={disabled} />
)}
<ExtItems disabled={disabled} extItems={extItems} />
{/* 协议 */}
{!disabled && (
<Space spacing={8} className={s['footer-draft']}>
<IconInfoCircle
style={{
fontSize: '16px',
color: '#4D53E8',
}}
/>
<span>
{I18n.t('plugin_create_draft_desc')}
<PluginDocs />
</span>
</Space>
)}
</>
)}
</Form>
</UIModal>
) : null}
</>
);
};

View File

@@ -0,0 +1,31 @@
/*
* 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 RegisterPluginMetaRequest,
type AuthorizationType,
type commonParamSchema,
} from '@coze-arch/bot-api/developer_api';
import { type UploadValue } from '@coze-common/biz-components';
export interface ConfirmFormProps
extends Omit<RegisterPluginMetaRequest, 'auth_type'> {
plugin_uri: UploadValue;
auth_type: Array<AuthorizationType>;
headerList?: Array<commonParamSchema>;
client_id?: string;
client_secret?: string;
}

View File

@@ -0,0 +1,130 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { safeJSONParse } from '@coze-arch/bot-utils';
import { type PluginMetaInfo } from '@coze-arch/bot-api/developer_api';
import { type UploadValue } from '@coze-common/biz-components';
export const formRuleList = {
name: [
{
required: true,
message: I18n.t('create_plugin_modal_name1_error'),
},
IS_OVERSEA
? {
pattern: /^[\w\s]+$/,
message: I18n.t('create_plugin_modal_nameerror'),
}
: {
pattern: /^[\w\s\u4e00-\u9fa5]+$/u, //
message: I18n.t('create_plugin_modal_nameerror_cn'),
},
],
desc: [
{
required: true,
message: I18n.t('create_plugin_modal_descrip1_error'),
},
IS_OVERSEA && {
// eslint-disable-next-line no-control-regex -- regex
pattern: /^[\x00-\x7F]+$/,
message: I18n.t('create_plugin_modal_descrip_error'),
},
],
url: [
{
required: true,
message: I18n.t('create_plugin_modal_url1_error'),
},
{
pattern: /^(https):\/\/.+$/,
message: I18n.t('create_plugin_modal_url_error_https'),
},
],
key: [
{
required: true,
message: I18n.t('create_plugin_modal_Parameter_error'),
},
{
// eslint-disable-next-line no-control-regex -- regex
pattern: /^[\x00-\x7F]+$/,
message: I18n.t('plugin_Parametename_error'),
},
],
service_token: [
{
required: true,
message: I18n.t('create_plugin_modal_Servicetoken_error'),
},
],
};
export const getPictureUploadInitValue = (
info?: PluginMetaInfo,
): UploadValue | undefined => {
if (!info) {
return;
}
return [
{
url: info.icon?.url || '',
uid: info?.icon?.uri || '',
},
];
};
export interface AuthOption {
label: string;
value: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- any
[key: string]: any;
}
/** 递归寻找auth选项下的输入项 */
// @ts-expect-error -- linter-disable-autofix
export const findAuthTypeItem = (data: AuthOption[], targetKey = 0) => {
for (const item of data) {
if (item.value === targetKey) {
return item;
} else if (item.children?.length > 0) {
return findAuthTypeItem(item.children, targetKey);
}
}
};
export function getAuthOptions(authSchema?: string): Array<AuthOption> {
const authOptions: AuthOption[] = [
{
label: I18n.t('create_plugin_modal_Authorization_no'),
value: 0,
key: 'None',
},
{
label: I18n.t('create_plugin_modal_Authorization_service'),
value: 1,
key: 'Service',
},
{
label: I18n.t('create_plugin_modal_Authorization_oauth'),
value: 3,
key: 'OAuth',
children: safeJSONParse(authSchema),
},
];
return authOptions;
}

View File

@@ -0,0 +1,51 @@
/*
* 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 { UIModal } from '@coze-arch/bot-semi';
import { IconWarningInfo } from '@coze-arch/bot-icons';
import { type DuplicateAPIInfo } from '@coze-arch/bot-api/plugin_develop';
interface MergeToolInfoProps {
onOk?: () => void;
onCancel?: () => void;
duplicateInfos?: DuplicateAPIInfo[];
}
export function showMergeTool({
duplicateInfos = [],
onCancel,
onOk,
}: MergeToolInfoProps) {
UIModal.warning({
title: I18n.t('duplicate_tools_within_plugin'),
content: duplicateInfos?.map(item => (
<div>{`${item.method} ${I18n.t('path_has_duplicates', {
path: item.path,
num: item.count,
})}`}</div>
)),
okText: I18n.t('merge_duplicate_tools'),
cancelText: I18n.t('Cancel'),
centered: true,
icon: <IconWarningInfo />,
okButtonProps: {
type: 'warning',
},
onOk,
onCancel,
});
}

View File

@@ -0,0 +1,282 @@
/*
* 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 { parse as yamlParse, stringify as yamlStringify } from 'yaml';
import { isObject } from 'lodash-es';
import axios from 'axios';
import { safeJSONParse } from '@coze-arch/bot-utils';
import { CustomError } from '@coze-arch/bot-error';
import { PluginDataFormat } from '@coze-arch/bot-api/plugin_develop';
import {
AuthorizationServiceLocation,
AuthorizationType,
ParameterLocation,
type commonParamSchema,
type PluginMetaInfo,
} from '@coze-arch/bot-api/developer_api';
interface PluginInfo {
aiPlugin?: string;
openAPI?: string;
}
interface AIPluginMetaInfo {
name_for_human?: string;
name_for_model?: string;
description_for_human?: string;
description_for_model?: string;
auth: {
type?: AIPluginAuthType;
client_url?: string;
authorization_url?: string;
authorization_content_type?: string;
platform?: string;
client_id?: string;
client_secret?: string;
location?: string;
key?: string;
service_token?: string;
scope?: string;
};
logo_url?: string;
common_params?: {
header?: Array<commonParamSchema>;
body?: Array<commonParamSchema>;
query?: Array<commonParamSchema>;
path?: Array<commonParamSchema>;
};
}
interface PluginInfoObject {
aiPlugin?: AIPluginMetaInfo;
openAPI?: {
info?: { description?: string; title?: string; version?: string };
servers?: Array<{ url?: string }>;
paths?: Array<unknown>;
[key: string]: unknown;
};
}
export enum AIPluginAuthType {
None = 'none',
Service = 'service_http',
OAuth = 'oauth',
}
export enum AIPluginAuthServiceLocation {
Header = 'Header',
Query = 'Query',
}
enum ImportFormatType {
Curl = 'curl',
OpenApi = 'openapi',
Postman = 'postman',
Unknown = '',
Swagger = 'swagger',
}
export function getFileExtension(name: string) {
const index = name.lastIndexOf('.');
return name.slice(index + 1);
}
// @ts-expect-error -- linter-disable-autofix
export async function getContent(file: Blob, onProgress): Promise<string> {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = event => {
const result = event.target?.result;
if (!result || typeof result !== 'string') {
reject(new CustomError('normal_error', 'file read fail'));
return;
}
resolve(result);
};
fileReader.onprogress = event => {
if (event.total) {
onProgress({
total: event.total,
loaded: event.loaded,
});
}
};
fileReader.readAsText(file);
});
}
export function isValidURL(str?: string): boolean {
// 缩略版
try {
const objExp = new RegExp(
'^(https?:\\/\\/)?' + // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name and extension
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
'(\\:\\d+)?' + // port
'(\\/[-a-z\\d%_.~+]*)*',
'i',
);
return Boolean(objExp.test(str || ''));
} catch (e) {
return false;
}
}
export async function customService(url: string) {
// 这里需要自定义请求需要引入axios
const axiosInstance = axios.create({ responseType: 'text' });
const response = await axiosInstance.get(url);
return response.data;
}
const AUTH_TYPE_MAP: Record<AIPluginAuthType, AuthorizationType> = {
[AIPluginAuthType.None]: AuthorizationType.None,
[AIPluginAuthType.Service]: AuthorizationType.Service,
[AIPluginAuthType.OAuth]: AuthorizationType.OAuth,
};
const AUTH_LOCATION_MAP: Record<
AIPluginAuthServiceLocation,
AuthorizationServiceLocation
> = {
[AIPluginAuthServiceLocation.Header]: AuthorizationServiceLocation.Header,
[AIPluginAuthServiceLocation.Query]: AuthorizationServiceLocation.Query,
};
export function parsePluginInfo(data: PluginInfo): PluginInfoObject {
const { aiPlugin, openAPI } = data;
const aiPluginObj = safeJSONParse(aiPlugin || '{}');
const openAPIObj = yamlParse(openAPI || '');
return {
aiPlugin: aiPluginObj,
openAPI: openAPIObj,
};
}
export function getInitialPluginMetaInfo(
data: PluginInfoObject,
): PluginMetaInfo {
const { aiPlugin, openAPI } = data;
const { type, location, key, service_token, ...oauthInfo } =
aiPlugin?.auth || {};
return {
name: aiPlugin?.name_for_human,
desc: aiPlugin?.description_for_human,
url: openAPI?.servers?.[0]?.url,
icon: { uri: aiPlugin?.logo_url },
auth_type: [AUTH_TYPE_MAP[type || AIPluginAuthType.None]],
// @ts-expect-error -- linter-disable-autofix
location: AUTH_LOCATION_MAP[location || ''],
key,
service_token,
oauth_info: JSON.stringify(oauthInfo),
common_params: {
[ParameterLocation.Header]: aiPlugin?.common_params?.header || [],
[ParameterLocation.Body]: aiPlugin?.common_params?.body || [],
[ParameterLocation.Path]: aiPlugin?.common_params?.path || [],
[ParameterLocation.Query]: aiPlugin?.common_params?.query || [],
},
};
}
function getKeyByValue<V>(
map: Record<string, V>,
value?: V,
): string | undefined {
if (value === undefined) {
return undefined;
}
for (const [key, val] of Object.entries(map)) {
if (val === value) {
return key;
}
}
}
export function getRegisterInfo(
pluginMetaInfo: PluginMetaInfo,
data: PluginInfo,
): PluginInfo {
const { aiPlugin: oriAIPluginInfo, openAPI: oriOpenAPIInfo } =
parsePluginInfo(data);
const {
name,
desc,
auth_type,
common_params,
location,
key,
service_token,
oauth_info,
icon,
} = pluginMetaInfo;
const newAIPlugin: AIPluginMetaInfo = {
name_for_human: name,
name_for_model: name,
description_for_human: desc,
description_for_model: desc,
logo_url: icon?.uri,
common_params: {
header: common_params?.[ParameterLocation.Header],
body: common_params?.[ParameterLocation.Body],
path: common_params?.[ParameterLocation.Path],
query: common_params?.[ParameterLocation.Query],
},
auth: {
type: getKeyByValue<AuthorizationType>(AUTH_TYPE_MAP, auth_type?.at(0)),
location: getKeyByValue<AuthorizationServiceLocation>(
AUTH_LOCATION_MAP,
location,
),
key,
service_token,
...JSON.parse(oauth_info || '{}'),
},
};
const mergedAIPluginInfo = { ...oriAIPluginInfo, ...newAIPlugin };
const mergedOpenAPIInfo = {
...(oriOpenAPIInfo || {}),
info: { ...(oriOpenAPIInfo?.info || {}), title: name, description: desc },
servers: [{ url: pluginMetaInfo.url }],
};
return {
aiPlugin: JSON.stringify(mergedAIPluginInfo),
openAPI: yamlStringify(mergedOpenAPIInfo),
};
}
export function getImportFormatType(
format?: PluginDataFormat,
): ImportFormatType {
switch (format) {
case PluginDataFormat.Curl:
return ImportFormatType.Curl;
case PluginDataFormat.OpenAPI:
return ImportFormatType.OpenApi;
case PluginDataFormat.Postman:
return ImportFormatType.Postman;
case PluginDataFormat.Swagger:
return ImportFormatType.Swagger;
default:
return ImportFormatType.Unknown;
}
}
export const isDuplicatePathErrorResponseData = (value: unknown): boolean =>
isObject(value) && 'paths_duplicated' in value;

View File

@@ -0,0 +1,47 @@
/*
* 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 { useMemo } from 'react';
import { I18n } from '@coze-arch/i18n';
import { Typography } from '@coze-arch/coze-design';
export const PluginDocs = () => {
const docsHref = useMemo(() => {
const DRAFT_CN = {
'zh-CN': '/docs/guides/plugin',
en: '/docs/en_guides/en_plugin',
};
const DRAFT_OVERSEA = {
'zh-CN': '',
en: '',
};
// @ts-expect-error -- linter-disable-autofix
return IS_OVERSEA ? DRAFT_OVERSEA[I18n.language] : DRAFT_CN[I18n.language];
}, []);
return !IS_OVERSEA ? (
<Typography.Text
link={{
href: docsHref,
target: '_blank',
}}
fontSize="12px"
>
{I18n.t('plugin_create_guide_link')}
</Typography.Text>
) : null;
};

View File

@@ -0,0 +1,14 @@
.wrapper {
display: flex;
flex: 1 1 auto;
align-items: center;
justify-content: flex-end;
padding-right: 20px;
}
.tip {
margin-right: 12px;
font-size: 14px;
color: rgba(29, 28, 35, 35%);
}

View File

@@ -0,0 +1,41 @@
/*
* 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 cs from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { UIButton } from '@coze-arch/bot-semi';
import { usePluginFeatModal } from '..';
import styles from './index.module.less';
export const PluginFeatButton: FC<{
className?: string;
}> = ({ className }) => {
const { modal, open } = usePluginFeatModal();
return (
<div className={cs(styles.wrapper, className)}>
{modal}
<span className={styles.tip}>{I18n.t('plugin_feedback_entry_tip')}</span>
<UIButton type="tertiary" onClick={open}>
{I18n.t('plugin_feedback_entry_button')}
</UIButton>
</div>
);
};

View File

@@ -0,0 +1,250 @@
/*
* 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, useCallback, useEffect, useRef, useState } from 'react';
import { debounce } from 'lodash-es';
import { useRequest } from 'ahooks';
import { I18n } from '@coze-arch/i18n';
import { SortType, ProductEntityType } from '@coze-arch/bot-api/product_api';
import { FeedbackType } from '@coze-arch/bot-api/plugin_develop';
import { ProductApi, PluginDevelopApi } from '@coze-arch/bot-api';
import {
Modal,
Form,
type FormApi,
Button as CozeButton,
type SelectProps,
type CommonFieldProps,
type ButtonProps,
Toast,
Tooltip,
Avatar,
} from '@coze-arch/coze-design';
const { Select, TextArea } = Form;
interface FormType {
feedback_type: FeedbackType;
plugin_id?: string;
feedback: string;
}
const options = [
{
value: FeedbackType.NotFoundPlugin,
label: I18n.t(
'plugin_feedback_modal_request_type_official_plugins_not_found',
),
},
{
value: FeedbackType.OfficialPlugin,
label: I18n.t(
'plugin_feedback_modal_request_type_feedback_to_existing_plugin',
),
},
];
const PluginSelect: FC<SelectProps & CommonFieldProps> = props => {
const [inputValue, setInputValue] = useState('');
const { data, loading } = useRequest(
async () => {
const res = await ProductApi.PublicGetProductList({
keyword: inputValue,
page_num: 1,
page_size: 50,
sort_type: SortType.Heat,
entity_type: ProductEntityType.Plugin,
is_official: true,
need_extra: false,
});
const products = res?.data?.products;
return products?.length ? products : [];
},
{
refreshDeps: [inputValue],
},
);
const pluginOptions = data
?.filter(p => p.meta_info)
.map(plugin => {
const meta = plugin.meta_info;
return {
value: meta.entity_id,
label: (
<>
{meta.icon_url ? (
<Avatar
size="extra-extra-small"
src={meta.icon_url}
shape="square"
className="mr-[5px]"
/>
) : null}
{meta.name}
</>
),
};
});
return (
<Select
onSearch={debounce((val, event) => {
if (event.type === 'change') {
setInputValue(val);
}
}, 800)}
loading={loading}
optionList={pluginOptions}
filter
remote
{...props}
/>
);
};
export const usePluginFeatModal = () => {
const [visible, setVisible] = useState(false);
const [submitting, setSubmitting] = useState(false);
const formApi = useRef<FormApi<FormType>>();
const onSubmit = async () => {
try {
setSubmitting(true);
const vals = await formApi.current?.validate();
const res = await PluginDevelopApi.CreatePluginFeedback(vals);
if (res?.code !== 0) {
return;
}
Toast.success(I18n.t('plugin_feedback_modal_tip_submission_success'));
setVisible(false);
} finally {
setSubmitting(false);
}
};
const reset = () => {
formApi.current?.setValues({
feedback_type: FeedbackType.NotFoundPlugin,
plugin_id: undefined,
feedback: '',
});
};
useEffect(() => {
if (!visible) {
return;
}
reset();
}, [visible]);
const modal = (
<Modal
title={I18n.t('plugin_feedback_modal_title')}
visible={visible}
onCancel={() => setVisible(false)}
width={562}
footer={
<>
<CozeButton
color="secondary"
onClick={() => setVisible(false)}
className="mr-[12px]"
>
{I18n.t('coze_home_delete_modal_btn_cancel')}
</CozeButton>
<CozeButton onClick={onSubmit} loading={submitting}>
{I18n.t('feedback_submit')}
</CozeButton>
</>
}
>
<Form<FormType>
getFormApi={api => (formApi.current = api)}
render={({ formState, values }) => (
<>
<Select
field="feedback_type"
rules={[
{
required: true,
},
]}
label={I18n.t('plugin_feedback_modal_request_type')}
optionList={options}
className="w-full"
/>
{values?.feedback_type === FeedbackType.OfficialPlugin && (
<PluginSelect
field="plugin_id"
rules={[
{
required: true,
message: I18n.t(
'plugin_feedback_error_tip_no_official_plugin_choosen',
),
},
]}
label={I18n.t('plugin_feedback_modal_choose_official_plugin')}
className="w-full"
/>
)}
<TextArea
field="feedback"
rules={[
{
required: true,
message: I18n.t('plugin_feedback_error_tip_empty_content'),
},
]}
label={I18n.t('plugin_feedback_modal_feedback_content')}
placeholder={I18n.t(
'plugin_feedback_modal_feedback_content_placeholder',
)}
maxCount={2000}
maxLength={2000}
/>
</>
)}
/>
</Modal>
);
const open = () => setVisible(true);
const EntryButton = useCallback<FC<ButtonProps>>(
props => (
<Tooltip content={I18n.t('plugin_feedback_entry_tip')}>
<CozeButton onClick={open} size="large" color="primary" {...props}>
{I18n.t('plugin_feedback_entry_button')}
</CozeButton>
</Tooltip>
),
[],
);
return {
open,
EntryButton,
modal,
};
};

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,129 @@
{
"$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-error/tsconfig.build.json"
},
{
"path": "../../../arch/bot-flags/tsconfig.build.json"
},
{
"path": "../../../arch/bot-hooks/tsconfig.build.json"
},
{
"path": "../../../arch/bot-http/tsconfig.build.json"
},
{
"path": "../../../arch/bot-monaco-editor/tsconfig.build.json"
},
{
"path": "../../../arch/bot-store/tsconfig.build.json"
},
{
"path": "../../../arch/bot-tea/tsconfig.build.json"
},
{
"path": "../../../arch/bot-typings/tsconfig.build.json"
},
{
"path": "../../../arch/bot-utils/tsconfig.build.json"
},
{
"path": "../../../arch/foundation-sdk/tsconfig.build.json"
},
{
"path": "../../../arch/i18n/tsconfig.build.json"
},
{
"path": "../../../arch/logger/tsconfig.build.json"
},
{
"path": "../../../common/assets/tsconfig.build.json"
},
{
"path": "../../../common/biz-components/tsconfig.build.json"
},
{
"path": "../../../common/flowgram-adapter/free-layout-editor/tsconfig.build.json"
},
{
"path": "../../../community/component/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": "../../../foundation/enterprise-store-adapter/tsconfig.build.json"
},
{
"path": "../mock-set/tsconfig.build.json"
},
{
"path": "../../plugin-modal-adapter/tsconfig.build.json"
},
{
"path": "../../plugin-shared/tsconfig.build.json"
},
{
"path": "../../../studio/bot-utils/tsconfig.build.json"
},
{
"path": "../../../studio/components/tsconfig.build.json"
},
{
"path": "../../../studio/plugin-form-adapter/tsconfig.build.json"
},
{
"path": "../../../studio/plugin-shared/tsconfig.build.json"
},
{
"path": "../../../studio/premium/premium-store-adapter/tsconfig.build.json"
},
{
"path": "../../../studio/stores/bot-detail/tsconfig.build.json"
},
{
"path": "../../../studio/stores/bot-plugin/tsconfig.build.json"
},
{
"path": "../../../studio/user-store/tsconfig.build.json"
},
{
"path": "../../tool/tsconfig.build.json"
},
{
"path": "../tools/tsconfig.build.json"
},
{
"path": "../../../workflow/base/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',
});