feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
const { defineConfig } = require('@coze-arch/stylelint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
extends: [],
|
||||
});
|
||||
16
frontend/packages/agent-ide/bot-plugin/export/README.md
Normal file
16
frontend/packages/agent-ide/bot-plugin/export/README.md
Normal 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`
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["./dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {},
|
||||
});
|
||||
133
frontend/packages/agent-ide/bot-plugin/export/package.json
Normal file
133
frontend/packages/agent-ide/bot-plugin/export/package.json
Normal 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 |
@@ -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%));
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
button.addbtn {
|
||||
width: 100%;
|
||||
margin-top: 24px;
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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?.();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -0,0 +1,3 @@
|
||||
.actions {
|
||||
margin-right: 20px;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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: '',
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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']}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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%);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
17
frontend/packages/agent-ide/bot-plugin/export/src/typings.d.ts
vendored
Normal file
17
frontend/packages/agent-ide/bot-plugin/export/src/typings.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
@@ -0,0 +1,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"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/packages/agent-ide/bot-plugin/export/tsconfig.json
Normal file
15
frontend/packages/agent-ide/bot-plugin/export/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
],
|
||||
"exclude": ["**/*"]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
});
|
||||
Reference in New Issue
Block a user