Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29c567977a | |||
| 2e32c57648 | |||
| c49bddb9e7 | |||
| 9617ddc02c | |||
| 8083090414 | |||
| 60296bf3c9 | |||
| 77d0d9bfb8 | |||
| 6e0c66d982 | |||
| 07b49030a2 | |||
| 203c3e4c4b | |||
| e434c7867f | |||
| f3059f8df8 | |||
| 0b0cd25d2c | |||
| 4e0239d282 | |||
| 2574b2814b | |||
| 1564bd977e | |||
| 92e47812bd | |||
| 2e867da093 | |||
| da85269c9f | |||
| 15f803a511 | |||
| 7eca309eac | |||
| 414308802c | |||
| 7333e4fda3 | |||
| 1cdaa29764 | |||
| 9d270b7b0e | |||
| 31abd609e6 | |||
| fdd1cd32a8 | |||
| 6210a06d43 | |||
| 807132014a | |||
| 041817cc3c | |||
| acb0ceb9a9 | |||
| 417dab7ddc | |||
| 3a9a07c109 | |||
| a02d505e2e | |||
| a0266b061f | |||
| f22738ec2b | |||
| fed8e62965 | |||
| ced2143a6e | |||
| a58e2c4e67 | |||
| 64c59e4886 | |||
| 49f353033c | |||
| eb7b9acb26 | |||
| 093c698cf0 | |||
| 977665fac7 | |||
| a153bfef02 | |||
| ccca43f3dc | |||
| bde2cca414 | |||
| d29d198c88 | |||
| cfafd043e9 | |||
| 4723114bf5 | |||
| 11e74fd08e | |||
| afc8fe4167 | |||
| f1333b68b6 | |||
| 69bbdb3d02 | |||
| 7645134b35 | |||
| 0250e65959 | |||
| fecfea5dad | |||
| cac95019b6 | |||
| 88022a8edd | |||
| b6ededa2c4 | |||
| b098a09425 | |||
| a1dcbd23b7 | |||
| b71615597b | |||
| 32b5f90e86 | |||
| 7ff4b77505 | |||
| 75a5416fad | |||
| 4c415a47f8 | |||
| 954c7f2192 | |||
| 1aa95fb972 | |||
| 5d71a8d7c5 | |||
| 4f7ca8018c | |||
| c167712b0e | |||
| a8b4599c4f | |||
| 25ad26ad55 | |||
| cf27da94a7 | |||
| a43880fbcc | |||
| e04282e3a4 | |||
| 98bde0a1c0 | |||
| 3e3227755b | |||
| 5c58283264 | |||
| 2c7680bedc | |||
| db2e05889d | |||
| 517ac1eeac | |||
| 63fbd35c63 | |||
| 6d55676aa9 | |||
| 48f8bc0c8e | |||
| 463f063932 | |||
| 11bc0331cf | |||
| 6af33ee5b7 | |||
| e289537fd9 | |||
| 7adbbc31c2 | |||
| 2e383282c8 | |||
| 7c913a4cb6 | |||
| 42e3384ebb | |||
| 8610cd5770 | |||
| 1f240581d2 | |||
| a0b2148b65 | |||
| 2619493362 | |||
| da37970a43 | |||
| 6879a97e36 | |||
| dd7f9c9f5f | |||
| 31a1703ba8 | |||
| 81a9c35cda | |||
| bd9e99890d | |||
| fe30521c24 | |||
| 06e7d96f67 | |||
| 666acb6241 | |||
| 1c499ae805 | |||
| 0153612020 | |||
| d27f7eba4d | |||
| 4c60acb5bb | |||
| 3de5221b4a | |||
| 3e0070db80 | |||
| b347d8f08c | |||
| 11ae2969b4 | |||
| b6d2abaa64 | |||
| 3e79f117a3 | |||
| 13517c835c | |||
| 8ec82bc9ba | |||
| 5205764fae | |||
| 16d1e8f3d7 | |||
| e6684244d1 | |||
| 976f3505bf | |||
| f91b83385f | |||
| 6797ff023d | |||
| 528bb727ad | |||
| e0c482efbc | |||
| cf285f0637 | |||
| d4c697d680 | |||
| d90dc847ef | |||
| 8707a01fd3 | |||
| 2c95e440d1 | |||
| b06c880a9c | |||
| b5d70ca524 | |||
| d41afdd2da | |||
| 5253a62f9a | |||
| 125f59b1e1 | |||
| e18a6ef1c6 | |||
| f65a908661 | |||
| 93f1a76efb | |||
| c36ff21b91 |
@@ -56,7 +56,7 @@ body:
|
||||
label: Environment
|
||||
description: Share your environment details. Reports without proper environment details will likely be closed.
|
||||
value: |
|
||||
- APINTO Dashboard version:
|
||||
- ApiPark version:
|
||||
- Operating system (run `uname -a`):
|
||||
validations:
|
||||
required: true
|
||||
@@ -1,7 +1,7 @@
|
||||
provider: bailian
|
||||
label:
|
||||
zh_Hans: 阿里云百炼
|
||||
en_US: bailian
|
||||
en_US: BaiLian
|
||||
icon_small:
|
||||
en_US: icon_s_en.svg
|
||||
icon_large:
|
||||
|
||||
@@ -89,7 +89,7 @@ provider_credential_schema:
|
||||
zh_Hans: 为了进行验证,请输入一个您可用的模型名称 (例如:amazon.titan-text-lite-v1)
|
||||
model_config:
|
||||
access_configuration_status: true
|
||||
access_configuration_demo: "{}"
|
||||
access_configuration_demo: "{\"region\":\"\",\"model\":\"\"}"
|
||||
address: https://bedrock-runtime.amazonaws.com
|
||||
sort: 4
|
||||
recommend: true
|
||||
@@ -1,11 +1 @@
|
||||
<svg width="80" height="22" viewBox="0 0 80 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Group">
|
||||
<path id="Vector" d="M25.1152 10.5768C25.1152 14.1739 27.4253 16.6819 30.6264 16.6819C33.8274 16.6819 36.1375 14.1739 36.1375 10.5768C36.1375 6.97973 33.8274 4.47168 30.6264 4.47168C27.4253 4.47168 25.1152 6.97973 25.1152 10.5768ZM34.0254 10.5768C34.0254 13.1509 32.6229 14.8174 30.6264 14.8174C28.6298 14.8174 27.2273 13.1509 27.2273 10.5768C27.2273 8.00275 28.6298 6.33622 30.6264 6.33622C32.6229 6.33622 34.0254 8.00275 34.0254 10.5768Z" fill="black"/>
|
||||
<path id="Vector_2" d="M42.0868 16.682C44.5124 16.682 45.8984 14.636 45.8984 12.1774C45.8984 9.71889 44.5124 7.67285 42.0868 7.67285C40.9648 7.67285 40.1398 8.11836 39.5953 8.76188V7.83786H37.6152V19.4706H39.5953V15.593C40.1398 16.2365 40.9648 16.682 42.0868 16.682ZM39.5458 11.9299C39.5458 10.2964 40.4698 9.40539 41.6908 9.40539C43.1264 9.40539 43.9019 10.5274 43.9019 12.1774C43.9019 13.8275 43.1264 14.9495 41.6908 14.9495C40.4698 14.9495 39.5458 14.042 39.5458 12.4415V11.9299Z" fill="black"/>
|
||||
<path id="Vector_3" d="M51.2545 16.682C52.987 16.682 54.3565 15.7745 54.967 14.2565L53.2675 13.613C53.0035 14.504 52.228 14.999 51.2545 14.999C49.9839 14.999 49.0929 14.0915 48.9444 12.6065H55.0165V11.9464C55.0165 9.57039 53.68 7.67285 51.172 7.67285C48.6639 7.67285 47.0469 9.63639 47.0469 12.1774C47.0469 14.8505 48.7794 16.682 51.2545 16.682ZM51.1555 9.33939C52.4095 9.33939 53.0035 10.1644 53.02 11.1214H49.0434C49.3404 9.9499 50.1324 9.33939 51.1555 9.33939Z" fill="black"/>
|
||||
<path id="Vector_4" d="M56.5038 16.5005H58.4838V11.4184C58.4838 10.1809 59.3913 9.52089 60.2824 9.52089C61.3714 9.52089 61.8004 10.2964 61.8004 11.3689V16.5005H63.7804V10.7914C63.7804 8.92688 62.6914 7.67285 60.8764 7.67285C59.7544 7.67285 58.9788 8.18436 58.4838 8.76188V7.83786H56.5038V16.5005Z" fill="black"/>
|
||||
<path id="Vector_5" d="M69.5799 4.65332L65.0918 16.5006H67.1873L68.1939 13.7945H73.309L74.332 16.5006H76.4605L71.9724 4.65332H69.5799ZM70.7349 6.99637L72.616 11.9465H68.8869L70.7349 6.99637Z" fill="black"/>
|
||||
<path id="Vector_6" d="M79.8581 4.6875H77.7461V16.5348H79.8581V4.6875Z" fill="black"/>
|
||||
<path id="Vector_7" d="M20.2769 9.00448C20.776 7.50639 20.6041 5.86529 19.8059 4.50264C18.6055 2.41259 16.1924 1.33732 13.8356 1.84333C12.7871 0.662179 11.2808 -0.00952316 9.70154 0.000102043C7.29248 -0.00539807 5.155 1.54563 4.41386 3.83781C2.86626 4.15475 1.53042 5.12346 0.748717 6.49643C-0.460621 8.58097 -0.184928 11.2087 1.43073 12.9962C0.931596 14.4943 1.10348 16.1354 1.90168 17.498C3.10208 19.5881 5.51526 20.6634 7.87206 20.1573C8.91983 21.3385 10.4269 22.0102 12.0061 21.9999C14.4165 22.0061 16.5547 20.4537 17.2958 18.1594C18.8434 17.8425 20.1793 16.8738 20.961 15.5008C22.1689 13.4163 21.8925 10.7906 20.2776 9.00311L20.2769 9.00448ZM12.0075 20.5623C11.0429 20.5637 10.1085 20.2261 9.36809 19.608C9.40178 19.5901 9.46022 19.5578 9.49803 19.5345L13.8789 17.0044C14.103 16.8772 14.2405 16.6386 14.2391 16.3808V10.2049L16.0906 11.274C16.1105 11.2836 16.1236 11.3028 16.1264 11.3248V16.4393C16.1236 18.7136 14.2818 20.5575 12.0075 20.5623ZM3.14952 16.7789C2.6662 15.9443 2.49225 14.9659 2.65795 14.0165C2.69026 14.0357 2.74732 14.0708 2.78789 14.0942L7.16873 16.6242C7.3908 16.7541 7.6658 16.7541 7.88856 16.6242L13.2367 13.5359V15.6741C13.2381 15.6961 13.2278 15.7174 13.2106 15.7311L8.78233 18.288C6.80985 19.4238 4.29079 18.7486 3.15021 16.7789H3.14952ZM1.99656 7.21626C2.47782 6.38024 3.23752 5.74085 4.14229 5.40878C4.14229 5.44659 4.14023 5.51328 4.14023 5.56003V10.6208C4.13885 10.878 4.27636 11.1165 4.4998 11.2437L9.84798 14.3313L7.9965 15.4004C7.97794 15.4128 7.95456 15.4149 7.93393 15.4059L3.50496 12.847C1.53661 11.7071 0.86147 9.18874 1.99587 7.21694L1.99656 7.21626ZM17.2085 10.7563L11.8603 7.66795L13.7118 6.59956C13.7304 6.58718 13.7537 6.58512 13.7744 6.59406L18.2033 9.15092C20.1751 10.2901 20.851 12.8126 19.7118 14.7844C19.2298 15.6191 18.4708 16.2584 17.5667 16.5912V11.3792C17.5688 11.122 17.432 10.8841 17.2092 10.7563H17.2085ZM19.0511 7.98284C19.0187 7.9629 18.9617 7.92852 18.9211 7.90515L14.5403 5.37509C14.3182 5.24515 14.0432 5.24515 13.8204 5.37509L8.47226 8.46341V6.32524C8.47088 6.30324 8.4812 6.28192 8.49838 6.26817L12.9267 3.71337C14.8991 2.57553 17.4209 3.25273 18.5581 5.2259C19.0387 6.05917 19.2126 7.03475 19.0497 7.98284H19.0511ZM7.46574 11.7937L5.61357 10.7246C5.59363 10.715 5.58057 10.6958 5.57782 10.6738V5.55935C5.5792 3.2823 7.42655 1.43701 9.7036 1.43838C10.6668 1.43838 11.5991 1.77664 12.3395 2.39265C12.3058 2.41053 12.2481 2.44284 12.2096 2.46622L7.82874 4.99627C7.60461 5.12346 7.46711 5.36134 7.46849 5.61916L7.46574 11.7924V11.7937ZM8.47157 9.62531L10.8538 8.24959L13.236 9.62462V12.3754L10.8538 13.7504L8.47157 12.3754V9.62531Z" fill="black"/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="56" viewBox="0 0 24 24" width="56" xmlns="http://www.w3.org/2000/svg" style="flex: 0 0 auto; line-height: 1;"><title>LM Studio</title><path d="M2.84 2a1.273 1.273 0 100 2.547h14.107a1.273 1.273 0 100-2.547H2.84zM7.935 5.33a1.273 1.273 0 000 2.548H22.04a1.274 1.274 0 000-2.547H7.935zM3.624 9.935c0-.704.57-1.274 1.274-1.274h14.106a1.274 1.274 0 010 2.547H4.898c-.703 0-1.274-.57-1.274-1.273zM1.273 12.188a1.273 1.273 0 100 2.547H15.38a1.274 1.274 0 000-2.547H1.273zM3.624 16.792c0-.704.57-1.274 1.274-1.274h14.106a1.273 1.273 0 110 2.547H4.898c-.703 0-1.274-.57-1.274-1.273zM13.029 18.849a1.273 1.273 0 100 2.547h9.698a1.273 1.273 0 100-2.547h-9.698z" fill-opacity=".3"></path><path d="M2.84 2a1.273 1.273 0 100 2.547h10.287a1.274 1.274 0 000-2.547H2.84zM7.935 5.33a1.273 1.273 0 000 2.548H18.22a1.274 1.274 0 000-2.547H7.935zM3.624 9.935c0-.704.57-1.274 1.274-1.274h10.286a1.273 1.273 0 010 2.547H4.898c-.703 0-1.274-.57-1.274-1.273zM1.273 12.188a1.273 1.273 0 100 2.547H11.56a1.274 1.274 0 000-2.547H1.273zM3.624 16.792c0-.704.57-1.274 1.274-1.274h10.286a1.273 1.273 0 110 2.547H4.898c-.703 0-1.274-.57-1.274-1.273zM13.029 18.849a1.273 1.273 0 100 2.547h5.78a1.273 1.273 0 100-2.547h-5.78z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 17 KiB |
@@ -29,12 +29,12 @@ provider_credential_schema:
|
||||
placeholder:
|
||||
zh_Hans: 在此输入您的 API Key
|
||||
en_US: Enter your API Key
|
||||
- variable: base_url
|
||||
- variable: dashscope_api_base
|
||||
label:
|
||||
en_US: https://api.baichuan-ai.com/v1
|
||||
en_US: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
type: text-input
|
||||
required: false
|
||||
placeholder:
|
||||
zh_Hans: 在此输入您的 Base URL
|
||||
en_US: Enter your Base URL
|
||||
address: https://api.baichuan-ai.com/v1
|
||||
address: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
@@ -81,7 +81,7 @@ func convertInt(value interface{}) int {
|
||||
|
||||
func genAIKey(key string, provider string) string {
|
||||
keys := strings.Split(key, "@")
|
||||
return strings.TrimSuffix(keys[0], fmt.Sprintf("-%s", provider))
|
||||
return strings.TrimPrefix(keys[0], fmt.Sprintf("%s-", provider))
|
||||
}
|
||||
|
||||
// HandleMessage 处理从 NSQ 读取的消息
|
||||
|
||||
@@ -267,7 +267,7 @@ func (i *imlLocalModelController) initAILocalService(ctx context.Context, model
|
||||
})
|
||||
|
||||
return func() error {
|
||||
path := fmt.Sprintf("/%s/chat", strings.Trim(prefix, "/"))
|
||||
path := fmt.Sprintf("/%s/chat/completions", strings.Trim(prefix, "/"))
|
||||
timeout := 300000
|
||||
retry := 0
|
||||
aiPrompt := &ai_api_dto.AiPrompt{
|
||||
|
||||
@@ -354,7 +354,8 @@ func (i *imlServiceController) createAIService(ctx *gin.Context, teamID string,
|
||||
if !has {
|
||||
return nil, fmt.Errorf("model %s not found", pv.DefaultLLM)
|
||||
}
|
||||
modelId = m.ID()
|
||||
//modelId = m.ID()
|
||||
modelId = m.Name()
|
||||
modelCfg = m.DefaultConfig()
|
||||
|
||||
}
|
||||
|
||||
@@ -395,36 +395,36 @@ func (i *imlInitController) createAIService(ctx context.Context, teamID string,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := fmt.Sprintf("/%s/demo_translation_api", strings.Trim(input.Prefix, "/"))
|
||||
path := fmt.Sprintf("/%s/chat/completions", strings.Trim(input.Prefix, "/"))
|
||||
timeout := 300000
|
||||
retry := 0
|
||||
aiPrompt := &ai_api_dto.AiPrompt{
|
||||
Variables: []*ai_api_dto.AiPromptVariable{
|
||||
{
|
||||
Key: "source_lang",
|
||||
Description: "",
|
||||
Require: true,
|
||||
},
|
||||
{
|
||||
Key: "target_lang",
|
||||
Description: "",
|
||||
Require: true,
|
||||
},
|
||||
{
|
||||
Key: "text",
|
||||
Description: "",
|
||||
Require: true,
|
||||
},
|
||||
},
|
||||
Prompt: "You need to translate {{source_lang}} into {{target_lang}}, and the following is the content that needs to be translated.\n---\n{{text}}",
|
||||
//Variables: []*ai_api_dto.AiPromptVariable{
|
||||
// {
|
||||
// Key: "source_lang",
|
||||
// Description: "",
|
||||
// Require: true,
|
||||
// },
|
||||
// {
|
||||
// Key: "target_lang",
|
||||
// Description: "",
|
||||
// Require: true,
|
||||
// },
|
||||
// {
|
||||
// Key: "text",
|
||||
// Description: "",
|
||||
// Require: true,
|
||||
// },
|
||||
//},
|
||||
//Prompt: "You need to translate {{source_lang}} into {{target_lang}}, and the following is the content that needs to be translated.\n---\n{{text}}",
|
||||
}
|
||||
aiModel := &ai_api_dto.AiModel{
|
||||
Id: m.ID(),
|
||||
Config: m.DefaultConfig(),
|
||||
Provider: providerId,
|
||||
}
|
||||
name := "Demo Translation API"
|
||||
description := "A demo that shows you how to use a prompt to create a Translation API."
|
||||
name := "Demo Chat API"
|
||||
description := "A demo that shows you how to use a prompt to create a Chat API."
|
||||
apiId := uuid.New().String()
|
||||
err = i.aiAPIModule.Create(
|
||||
ctx,
|
||||
|
||||
@@ -47,7 +47,10 @@
|
||||
"swagger-ui-react": "^5.17.14",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"uuid": "^9.0.1",
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"react-json-view": "^1.21.3",
|
||||
"zod": "^3.23.8",
|
||||
"@modelcontextprotocol/sdk": "^1.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/cssinjs": "^1.18.2",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useNavigate } from 'react-router-dom'
|
||||
class InsidePageProps {
|
||||
showBanner?: boolean = true
|
||||
pageTitle: string | React.ReactNode = ''
|
||||
tagList?: Array<{ label: string | ReactNode }> = []
|
||||
tagList?: Array<{ label: string | ReactNode; className?: string; color?: string }> = []
|
||||
children: React.ReactNode
|
||||
showBtn?: boolean = false
|
||||
btnTitle?: string = ''
|
||||
@@ -79,7 +79,7 @@ const InsidePage: FC<InsidePageProps> = ({
|
||||
tagList?.length > 0 &&
|
||||
tagList?.map((tag) => {
|
||||
return (
|
||||
<Tag key={tag.label as string} bordered={false}>
|
||||
<Tag key={tag.label as string} bordered={false} color={tag.color} className={tag.className}>
|
||||
{tag.label}
|
||||
</Tag>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { App, Col, Form, Input, Row, Table, Tooltip } from 'antd'
|
||||
import { forwardRef, useEffect, useImperativeHandle, useMemo } from 'react'
|
||||
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react'
|
||||
import {
|
||||
PublishApprovalInfoType,
|
||||
PublishApprovalModalHandle,
|
||||
@@ -36,6 +36,7 @@ export const PublishApprovalModalContent = forwardRef<PublishApprovalModalHandle
|
||||
const [form] = Form.useForm()
|
||||
const { fetchData } = useFetch()
|
||||
const { state } = useGlobalContext()
|
||||
const versionInputRef = useRef<Input>(null)
|
||||
|
||||
const save: (operate: 'pass' | 'refuse') => Promise<boolean | string> = (operate) => {
|
||||
if (type === 'view') {
|
||||
@@ -140,6 +141,12 @@ export const PublishApprovalModalContent = forwardRef<PublishApprovalModalHandle
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({ opinion: '', ...data })
|
||||
// 如果是添加模式且insidePage为true,自动聚焦版本号输入框
|
||||
if (type === 'add' && insidePage && versionInputRef.current) {
|
||||
setTimeout(() => {
|
||||
versionInputRef.current?.focus()
|
||||
}, 100)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const translatedUpstreamColumns = useMemo(
|
||||
@@ -335,7 +342,12 @@ export const PublishApprovalModalContent = forwardRef<PublishApprovalModalHandle
|
||||
{insidePage && (
|
||||
<>
|
||||
<Form.Item label={$t('版本号')} name="version" rules={[{ required: true, whitespace: true }]}>
|
||||
<Input className="w-INPUT_NORMAL" disabled={type !== 'add'} placeholder={$t(PLACEHOLDER.input)} />
|
||||
<Input
|
||||
className="w-INPUT_NORMAL"
|
||||
disabled={type !== 'add'}
|
||||
placeholder={$t(PLACEHOLDER.input)}
|
||||
ref={versionInputRef}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={$t('版本说明')} name="versionRemark">
|
||||
|
||||
@@ -196,6 +196,20 @@ const mockData = [
|
||||
key: 'maintenanceCenter',
|
||||
path: '/datasourcing',
|
||||
children: [
|
||||
{
|
||||
name: 'MCP 服务',
|
||||
key: 'mcpService',
|
||||
path: '/mcpService',
|
||||
icon: 'ph:network-x',
|
||||
access: ''
|
||||
},
|
||||
{
|
||||
name: 'MCP Key',
|
||||
key: 'mcpKey',
|
||||
path: '/mcpKey',
|
||||
icon: 'material-symbols:key',
|
||||
access: ''
|
||||
},
|
||||
{
|
||||
name: '数据源',
|
||||
key: 'datasourcing',
|
||||
|
||||
@@ -220,6 +220,26 @@ const mockData = {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
driver: 'apipark.builtIn.component',
|
||||
name: 'mcpService',
|
||||
router: [
|
||||
{
|
||||
path: 'mcpService',
|
||||
type: 'normal'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
driver: 'apipark.builtIn.component',
|
||||
name: 'mcpKey',
|
||||
router: [
|
||||
{
|
||||
path: 'mcpKey',
|
||||
type: 'normal'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
driver: 'apipark.builtIn.component',
|
||||
name: 'loadBalancing',
|
||||
|
||||
@@ -898,5 +898,7 @@
|
||||
"Kce2fcdbf": "No Permission",
|
||||
"K24f6a5b4": "Custom (Empty Template)",
|
||||
"Kea608112": "Load Preset Template",
|
||||
"Kee7de862": "Edit Provider( (0) )"
|
||||
"Kee7de862": "Edit Provider( (0) )",
|
||||
"Kb0e0aeda": "New API Key",
|
||||
"K9d81999c": "The API Key can be used to call system-level Open API and MCP."
|
||||
}
|
||||
|
||||
@@ -920,5 +920,7 @@
|
||||
"Kce2fcdbf": "権限がありません",
|
||||
"K24f6a5b4": "カスタム(空のテンプレート)",
|
||||
"Kea608112": "プリセットテンプレートを読み込む",
|
||||
"Kee7de862": "サプライヤーを編集( (0) )"
|
||||
"Kee7de862": "サプライヤーを編集( (0) )",
|
||||
"Kb0e0aeda": "APIキーを新規作成",
|
||||
"K9d81999c": "APIキーは、システムレベルのOpen APIおよびMCPの呼び出しに使用できます。"
|
||||
}
|
||||
|
||||
@@ -851,5 +851,7 @@
|
||||
"Kce2fcdbf": "暂无权限",
|
||||
"K24f6a5b4": "自定义(空模板)",
|
||||
"Kea608112": "载入预置模板",
|
||||
"Kee7de862": "编辑供应商( (0) )"
|
||||
"Kee7de862": "编辑供应商( (0) )",
|
||||
"Kb0e0aeda": "新增 API Key",
|
||||
"K9d81999c": "API 密钥可用于调用系统级 Open API 和 MCP。"
|
||||
}
|
||||
|
||||
@@ -920,5 +920,7 @@
|
||||
"Kce2fcdbf": "暫無權限",
|
||||
"K24f6a5b4": "自訂(空模板)",
|
||||
"Kea608112": "載入預設模板",
|
||||
"Kee7de862": "編輯供應商( (0) )"
|
||||
"Kee7de862": "編輯供應商( (0) )",
|
||||
"Kb0e0aeda": "新增 API 金鑰",
|
||||
"K9d81999c": "API 金鑰可用於調用系統級 Open API 和 MCP。"
|
||||
}
|
||||
|
||||
@@ -91,8 +91,15 @@ const AIProviderSelect: React.FC<AIProviderSelectProps> = ({ value, onChange, so
|
||||
label: (
|
||||
<Space className="flex items-center">
|
||||
<span
|
||||
className="flex items-center h-[20px] w-[20px]"
|
||||
dangerouslySetInnerHTML={{ __html: provider.logo }}
|
||||
className="flex items-center justify-center h-[20px] w-[20px] overflow-hidden"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: provider.logo.replace(/<svg/, '<svg style="max-width:100%;max-height:100%;width:auto;height:auto;"')
|
||||
}}
|
||||
></span>
|
||||
<span>{provider.name}</span>
|
||||
</Space>
|
||||
|
||||
@@ -21,6 +21,7 @@ export type AiServiceConfigFieldType = {
|
||||
catalogue?:string | string[];
|
||||
approvalType?:string;
|
||||
providerType?:string
|
||||
enable_mcp?: boolean
|
||||
};
|
||||
|
||||
export type AiServiceSubServiceTableListItem = {
|
||||
|
||||
@@ -800,6 +800,22 @@ export const routerMap: Map<string, RouterMapConfig> = new Map([
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
'mcpService',
|
||||
{
|
||||
type: 'module',
|
||||
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/mcpService/McpServiceContainer')),
|
||||
key: 'mcpService'
|
||||
}
|
||||
],
|
||||
[
|
||||
'mcpKey',
|
||||
{
|
||||
type: 'module',
|
||||
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/mcpService/McpKeyContainer')),
|
||||
key: 'mcpKey'
|
||||
}
|
||||
],
|
||||
[
|
||||
'loadBalancing',
|
||||
{
|
||||
|
||||
@@ -362,6 +362,10 @@ export const SERVICE_APPROVAL_OPTIONS = [
|
||||
{ label: '无需审核:允许任何消费者调用该服务', value: 'auto' },
|
||||
{ label: '人工审核:仅允许通过人工审核的消费者调用该服务', value: 'manual' }
|
||||
]
|
||||
export const MCP_OPTIONS = [
|
||||
{ label: '关闭', value: false },
|
||||
{ label: '开启:AI Agent 等产品能够通过 MCP 方式调用服务', value: true }
|
||||
]
|
||||
export const SERVICE_KIND_OPTIONS = [
|
||||
{ label: 'REST', value: 'rest' },
|
||||
{ label: 'AI', value: 'ai' }
|
||||
|
||||
@@ -31,6 +31,7 @@ export type SystemConfigFieldType = {
|
||||
catalogue?:string | string[];
|
||||
approvalType?:string;
|
||||
modelMapping?: string;
|
||||
enable_mcp?: boolean;
|
||||
};
|
||||
|
||||
export type SystemSubServiceTableListItem = {
|
||||
|
||||
@@ -666,6 +666,15 @@ p{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.apipark-layout-base-menu-horizontal-menu-item {
|
||||
padding-right: 0px !important;
|
||||
padding-left: 0px !important;
|
||||
}
|
||||
.apipark-layout-base-menu-horizontal-item-title {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.ant-pro-table-list-toolbar-setting-items{
|
||||
position:absolute;
|
||||
top:18px;
|
||||
|
||||
@@ -228,6 +228,7 @@ const AiServiceInsidePage: FC = () => {
|
||||
<InsidePage
|
||||
pageTitle={aiServiceInfo?.name || '-'}
|
||||
tagList={[
|
||||
...(aiServiceInfo?.enable_mcp ? [{ label: 'MCP', color: '#ffc107', className: 'text-[#000]' }] : []),
|
||||
{
|
||||
label: (
|
||||
<Paragraph className="mb-0" copyable={serviceId ? { text: serviceId } : false}>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { DefaultOptionType } from 'antd/es/select'
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
|
||||
|
||||
export type AiServiceRouterModelConfigHandle = {
|
||||
save: () => Promise<{ id: string; config: string, type: string, provider: string }>
|
||||
save: () => Promise<{ id: string; config: string; type: string; provider: string }>
|
||||
}
|
||||
|
||||
export type AiServiceRouterModelConfigProps = {
|
||||
@@ -49,7 +49,7 @@ const AiServiceRouterModelConfig = forwardRef<AiServiceRouterModelConfigHandle,
|
||||
|
||||
/**
|
||||
* 获取本地模型列表
|
||||
* @param setDefaultValue
|
||||
* @param setDefaultValue
|
||||
*/
|
||||
const getLocalLlmList = (setDefaultValue?: boolean) => {
|
||||
fetchData<LocalLlmType[]>('simple/ai/models/local/configured', {
|
||||
@@ -113,12 +113,11 @@ const AiServiceRouterModelConfig = forwardRef<AiServiceRouterModelConfigHandle,
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setProviderList(
|
||||
data.providers
|
||||
?.map((x: SimpleAiProviderItem) => {
|
||||
return { ...x, label: x.name, value: x.id }
|
||||
})
|
||||
data.providers?.map((x: SimpleAiProviderItem) => {
|
||||
return { ...x, label: x.name, value: x.id }
|
||||
})
|
||||
)
|
||||
if (setDefaultValue && data.providers.length) {
|
||||
if (setDefaultValue && data.providers.length) {
|
||||
const id = data.providers[0].id
|
||||
form.setFieldValue('provider', id)
|
||||
getLlmList(id)
|
||||
@@ -179,9 +178,14 @@ const AiServiceRouterModelConfig = forwardRef<AiServiceRouterModelConfigHandle,
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
className="w-INPUT_NORMAL"
|
||||
filterOption={(input, option) => (option?.searchText ?? '').includes(input.toLowerCase())}
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={providerList}
|
||||
options={providerList.map((x) => ({
|
||||
...x,
|
||||
searchText: x.name.toLowerCase()
|
||||
}))}
|
||||
onChange={(e) => {
|
||||
getLlmList(e)
|
||||
}}
|
||||
@@ -191,19 +195,20 @@ const AiServiceRouterModelConfig = forwardRef<AiServiceRouterModelConfigHandle,
|
||||
|
||||
<Form.Item<AiServiceRouterModelConfigField> label={$t('模型')} name="id" rules={[{ required: true }]}>
|
||||
<Select
|
||||
showSearch
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={
|
||||
llmList?.map((x) => ({
|
||||
value: x.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-[10px]" key={x.id}>
|
||||
<span>{x.name || x.id}</span>
|
||||
{modelType === 'online' && x?.scopes?.map((s: any) => <Tag>{s?.toLocaleUpperCase()}</Tag>)}
|
||||
</div>
|
||||
)
|
||||
}))
|
||||
}
|
||||
filterOption={(input, option) => (option?.searchText ?? '').includes(input.toLowerCase())}
|
||||
options={llmList?.map((x) => ({
|
||||
value: x.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-[10px]" key={x.id}>
|
||||
<span>{x.name || x.id}</span>
|
||||
{modelType === 'online' && x?.scopes?.map((s: any) => <Tag>{s?.toLocaleUpperCase()}</Tag>)}
|
||||
</div>
|
||||
),
|
||||
searchText: x.name.toLowerCase()
|
||||
}))}
|
||||
onChange={(e) => {
|
||||
form.setFieldValue('config', llmList.find((x) => x.id === e)?.config)
|
||||
}}
|
||||
|
||||
@@ -283,8 +283,9 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
||||
),
|
||||
onOk: () => {
|
||||
return addModelModalRef.current?.save().then((res) => {
|
||||
if (res === true) {
|
||||
if (res) {
|
||||
getLlmList(lastLlmID)
|
||||
form.setFieldValue('defaultLlm', res)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -102,7 +102,7 @@ const AddModels = forwardRef<addModelsContentHandle, addModelContentProps>((prop
|
||||
...value,
|
||||
id: modelID
|
||||
}
|
||||
fetchData<BasicResponse<null>>('ai/provider/model', {
|
||||
fetchData<BasicResponse<{ model: { id: string, name: string } }>>('ai/provider/model', {
|
||||
method: type === 'edit' ? 'PUT' : 'POST',
|
||||
eoParams: { provider: providerID },
|
||||
eoBody: finalValue,
|
||||
@@ -112,7 +112,8 @@ const AddModels = forwardRef<addModelsContentHandle, addModelContentProps>((prop
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success($t(RESPONSE_TIPS.success) || msg)
|
||||
resolve(true)
|
||||
const llmId = response.data?.model?.id
|
||||
resolve(llmId)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
reject(msg || $t(RESPONSE_TIPS.error))
|
||||
|
||||
@@ -182,7 +182,9 @@ const AddLoadBalancingModel = forwardRef<LoadBalancingHandle>((props, ref: any)
|
||||
{modelType === 'online' && (
|
||||
<Form.Item<LoadModelDetailData> label={$t('模型供应商')} name="provider" rules={[{ required: true }]}>
|
||||
<Select
|
||||
showSearch
|
||||
className="w-INPUT_NORMAL"
|
||||
filterOption={(input, option) => (option?.searchText ?? '').includes(input.toLowerCase())}
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
loading={modelProviderLoading}
|
||||
options={modelProviderData?.map((x) => ({
|
||||
@@ -191,7 +193,8 @@ const AddLoadBalancingModel = forwardRef<LoadBalancingHandle>((props, ref: any)
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<span>{x.name}</span>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
searchText: x.name.toLowerCase()
|
||||
}))}
|
||||
onChange={(e) => {
|
||||
modelProviderChange(e)
|
||||
@@ -201,8 +204,10 @@ const AddLoadBalancingModel = forwardRef<LoadBalancingHandle>((props, ref: any)
|
||||
)}
|
||||
<Form.Item label={$t('模型')} name="model" className="mt-[16px]" rules={[{ required: true }]}>
|
||||
<Select
|
||||
showSearch
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.input)}
|
||||
filterOption={(input, option) => (option?.searchText ?? '').includes(input.toLowerCase())}
|
||||
loading={llmListLoading}
|
||||
options={
|
||||
llmList?.map((x) => ({
|
||||
@@ -212,7 +217,8 @@ const AddLoadBalancingModel = forwardRef<LoadBalancingHandle>((props, ref: any)
|
||||
<span>{x.name || x.id}</span>
|
||||
{ modelType === 'online' &&x?.scopes?.map((s: any) => <Tag key={s}>{s?.toLocaleUpperCase()}</Tag>)}
|
||||
</div>
|
||||
)
|
||||
),
|
||||
searchText: x.name.toLowerCase()
|
||||
}))
|
||||
}
|
||||
onChange={(value) => {
|
||||
|
||||
@@ -40,7 +40,7 @@ const LogSettings = () => {
|
||||
const menuData = useMemo(() => {
|
||||
const newMenu = menuItems?.map((x: DynamicMenuItem) => {
|
||||
return getItem(
|
||||
<Link to={`template/${x.name}`}>{$t(x.title)}</Link>,
|
||||
<Link to={`/logsettings/template/${x.name}`}>{$t(x.title)}</Link>,
|
||||
x.name,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { App, Form, Input } from 'antd'
|
||||
import { $t } from '@common/locales'
|
||||
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { forwardRef, useEffect, useImperativeHandle } from 'react'
|
||||
type modelFieldType = {
|
||||
name: string
|
||||
type: string
|
||||
model_parameters: string
|
||||
access_configuration: string
|
||||
}
|
||||
|
||||
export type addMcpKeysHandle = {
|
||||
save: () => Promise<boolean | string>
|
||||
}
|
||||
|
||||
type addMcpKeysProps = {
|
||||
name?: string
|
||||
value?: string
|
||||
type?: string
|
||||
apikey?: string
|
||||
}
|
||||
|
||||
const AddMcpKey = forwardRef<addMcpKeysHandle, addMcpKeysProps>((props, ref) => {
|
||||
const { name = '', value: editValue = '', type = 'new', apikey = '' } = props
|
||||
const [form] = Form.useForm()
|
||||
const { message } = App.useApp()
|
||||
const { fetchData } = useFetch()
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({
|
||||
name,
|
||||
value: editValue
|
||||
})
|
||||
}, [])
|
||||
/**
|
||||
* 保存
|
||||
* @returns
|
||||
*/
|
||||
const save: () => Promise<boolean | string> = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
form
|
||||
.validateFields()
|
||||
.then((value) => {
|
||||
console.log('value', value)
|
||||
const finalValue = {
|
||||
...value,
|
||||
value: editValue ? editValue : uuidv4(),
|
||||
expired: 0
|
||||
}
|
||||
fetchData<BasicResponse<any>>('system/apikey', {
|
||||
method: type === 'new' ? 'POST' : 'PUT',
|
||||
eoBody: finalValue,
|
||||
...(type === 'edit' ? {
|
||||
eoParams: { apikey }
|
||||
} : {})
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success($t(RESPONSE_TIPS.success) || msg)
|
||||
resolve(true)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => reject(errorInfo))
|
||||
})
|
||||
.catch((errorInfo) => reject(errorInfo))
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
save
|
||||
}))
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
labelAlign="left"
|
||||
scrollToFirstError
|
||||
className="flex flex-col mx-auto h-full"
|
||||
name="mcpKeyModalConfig"
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item<modelFieldType> label={$t('名称')} name="name" rules={[{ required: true }]}>
|
||||
<Input autoFocus className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
})
|
||||
|
||||
export default AddMcpKey
|
||||
@@ -0,0 +1,316 @@
|
||||
import { App, Card, Select } from 'antd'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import { useEffect, useState } from 'react'
|
||||
import ReactJson from 'react-json-view'
|
||||
import { IconButton } from '@common/components/postcat/api/IconButton'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { useConnection } from './hook/useConnection'
|
||||
|
||||
type ConfigList = {
|
||||
openApi?: {
|
||||
title: string
|
||||
configContent: string
|
||||
apiKeys: string[]
|
||||
}
|
||||
mcp: {
|
||||
title: string
|
||||
configContent: string
|
||||
apiKeys: string[]
|
||||
}
|
||||
}
|
||||
|
||||
type ApiKeyItem = {
|
||||
expired: number
|
||||
id: string
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
|
||||
const IntegrationAIContainer = ({ type, handleApiKeyChange }: { type: 'global' | 'service'; handleApiKeyChange: (value: string) => void }) => {
|
||||
const [activeTab, setActiveTab] = useState('mcp')
|
||||
const { message } = App.useApp()
|
||||
const [configContent, setConfigContent] = useState<string>('')
|
||||
const [apiKey, setApiKey] = useState<string>('')
|
||||
const [apiKeyList, setApiKeyList] = useState<{ value: string; label: string }[]>([])
|
||||
const [mcpServerUrl, setMcpServerUrl] = useState<string>('')
|
||||
const [tabContent, setTabContent] = useState<ConfigList>({
|
||||
mcp: {
|
||||
title: $t('MCP 配置'),
|
||||
configContent: '',
|
||||
apiKeys: []
|
||||
}
|
||||
})
|
||||
const { fetchData } = useFetch()
|
||||
|
||||
const initTabsData = () => {
|
||||
const params: ConfigList = {
|
||||
mcp: {
|
||||
title: $t('MCP 配置'),
|
||||
configContent: '',
|
||||
apiKeys: []
|
||||
}
|
||||
}
|
||||
if (type === 'global') {
|
||||
params.openApi = {
|
||||
title: $t('Open API 文档'),
|
||||
configContent: '',
|
||||
apiKeys: []
|
||||
}
|
||||
}
|
||||
setTabContent(params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
const handleCopy = async (value: string): Promise<void> => {
|
||||
if (value) {
|
||||
await navigator.clipboard.writeText(value)
|
||||
message.success($t(RESPONSE_TIPS.copySuccess))
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setApiKey(value)
|
||||
handleApiKeyChange(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局 MCP 配置
|
||||
* @returns
|
||||
*/
|
||||
const getGlobalMcpConfig = () => {
|
||||
fetchData<BasicResponse<null>>('global/mcp/config', {
|
||||
method: 'GET'
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg, data } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setTabContent((prevTabContent) => ({
|
||||
...prevTabContent,
|
||||
mcp: {
|
||||
...prevTabContent.mcp,
|
||||
configContent: data.config || ''
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
message.error(errorInfo || $t(RESPONSE_TIPS.error))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 API Key 列表
|
||||
*/
|
||||
const getKeysList = () => {
|
||||
fetchData<BasicResponse<null>>(type === 'global' ? 'simple/system/apikeys' : '', {
|
||||
method: 'GET'
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg, data } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
if (data.apikeys && data.apikeys.length > 0) {
|
||||
setApiKeyList(
|
||||
data.apikeys.map((item: ApiKeyItem) => {
|
||||
return {
|
||||
label: item.name,
|
||||
value: item.value
|
||||
}
|
||||
})
|
||||
)
|
||||
setApiKey(data.apikeys[0].value)
|
||||
}
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
message.error(errorInfo || $t(RESPONSE_TIPS.error))
|
||||
})
|
||||
}
|
||||
|
||||
const {
|
||||
connectionStatus,
|
||||
serverCapabilities,
|
||||
mcpClient,
|
||||
requestHistory,
|
||||
makeRequest: makeConnectionRequest,
|
||||
sendNotification,
|
||||
handleCompletion,
|
||||
completionsSupported,
|
||||
connect: connectMcpServer,
|
||||
disconnect: disconnectMcpServer,
|
||||
} = useConnection({
|
||||
transportType: 'sse',
|
||||
sseUrl: mcpServerUrl,
|
||||
proxyServerUrl: 'mcp/global/sse',
|
||||
requestTimeout: 1000,
|
||||
});
|
||||
console.log('connectionStatus==================', connectionStatus);
|
||||
// console.log('serverCapabilities==================', serverCapabilities);
|
||||
// console.log('mcpClient==================', mcpClient);
|
||||
// console.log('requestHistory==================', requestHistory);
|
||||
// console.log('makeConnectionRequest==================', makeConnectionRequest);
|
||||
// console.log('sendNotification==================', sendNotification);
|
||||
// console.log('handleCompletion==================', handleCompletion);
|
||||
// console.log('completionsSupported==================', completionsSupported);
|
||||
// console.log('connectMcpServer==================', connectMcpServer);
|
||||
// console.log('disconnectMcpServer==================', disconnectMcpServer);
|
||||
// const useConnectAIagent = () => {
|
||||
// connectMcpServer()
|
||||
// }
|
||||
|
||||
useEffect(() => {
|
||||
type === 'global' && getGlobalMcpConfig()
|
||||
initTabsData()
|
||||
getKeysList()
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (activeTab === 'openApi') {
|
||||
setConfigContent(tabContent.openApi?.configContent || '')
|
||||
} else if (activeTab === 'mcp') {
|
||||
setConfigContent(tabContent.mcp.configContent || '')
|
||||
}
|
||||
}, [tabContent, activeTab])
|
||||
useEffect(() => {
|
||||
if (configContent && apiKey) {
|
||||
const parsedConfig = JSON.parse(configContent)
|
||||
console.log('啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊parsedConfig', parsedConfig, apiKey)
|
||||
let baseUrl = ''
|
||||
if (parsedConfig?.mcpServers) {
|
||||
// 获取 mcpServers 对象中的第一个键
|
||||
const serverKey = Object.keys(parsedConfig.mcpServers)[0]
|
||||
baseUrl = parsedConfig.mcpServers[serverKey]?.url
|
||||
}
|
||||
baseUrl = baseUrl.replace('{your_api_key}', apiKey)
|
||||
if (mcpServerUrl === baseUrl) {
|
||||
return
|
||||
}
|
||||
setMcpServerUrl(baseUrl)
|
||||
console.log('啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊', mcpServerUrl)
|
||||
if (connectionStatus === 'connected') {
|
||||
disconnectMcpServer()
|
||||
}
|
||||
connectMcpServer()
|
||||
}
|
||||
}, [apiKey, configContent, connectMcpServer])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
style={{ borderRadius: '10px' }}
|
||||
className="w-[400px]"
|
||||
classNames={{
|
||||
body: 'p-[10px]'
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
<Icon
|
||||
icon="icon-park-solid:connection-point-two"
|
||||
className="align-text-bottom mr-[5px]"
|
||||
width="16"
|
||||
height="16"
|
||||
/>
|
||||
{$t('AI 代理集成')}
|
||||
</p>
|
||||
<div className="tab-container mt-3">
|
||||
{type === 'service' && (
|
||||
<div className="tab-nav flex rounded-md overflow-hidden border border-solid border-[#3D46F2] w-fit">
|
||||
<div
|
||||
className={`tab-item px-5 py-1.5 cursor-pointer text-sm transition-colors ${activeTab === 'openApi' ? 'bg-[#3D46F2] text-white' : 'bg-white text-[#3D46F2]'}`}
|
||||
onClick={() => setActiveTab('openApi')}
|
||||
>
|
||||
Open API
|
||||
</div>
|
||||
<div
|
||||
className={`tab-item px-5 py-1.5 cursor-pointer text-sm transition-colors ${activeTab === 'mcp' ? 'bg-[#3D46F2] text-white' : 'bg-white text-[#3D46F2]'}`}
|
||||
onClick={() => setActiveTab('mcp')}
|
||||
>
|
||||
MCP
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="tab-content font-semibold mt-[10px]">
|
||||
{activeTab === 'openApi' ? tabContent.openApi?.title : tabContent.mcp.title}
|
||||
</div>
|
||||
{/* 标签页内容区域 */}
|
||||
<div className="bg-[#0a0b21] text-white p-4 rounded-md my-2 font-mono text-sm overflow-auto relative">
|
||||
<ReactJson
|
||||
src={configContent ? JSON.parse(configContent) : {}}
|
||||
theme="monokai"
|
||||
indentWidth={2}
|
||||
displayDataTypes={false}
|
||||
displayObjectSize={false}
|
||||
name={false}
|
||||
collapsed={false}
|
||||
enableClipboard={false}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal'
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
name="copy"
|
||||
onClick={() => handleCopy(configContent)}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '5px',
|
||||
right: '5px',
|
||||
color: '#999',
|
||||
transition: 'none',
|
||||
'&.MuiButtonBase-root:hover': {
|
||||
background: 'transparent',
|
||||
color: '#3D46F2',
|
||||
transition: 'none'
|
||||
}
|
||||
}}
|
||||
></IconButton>
|
||||
</div>
|
||||
</div>
|
||||
{activeTab === 'mcp' && (
|
||||
<>
|
||||
<div className="tab-content font-semibold my-[10px]">API Key</div>
|
||||
<Select value={apiKey} className="w-full" onChange={handleChange} options={apiKeyList} />
|
||||
<Card
|
||||
style={{ borderRadius: '5px' }}
|
||||
className="w-full mt-[5px] "
|
||||
classNames={{
|
||||
body: 'p-[5px]'
|
||||
}}
|
||||
>
|
||||
<div className="relative h-[25px]">
|
||||
{apiKey}
|
||||
<IconButton
|
||||
name="copy"
|
||||
onClick={() => handleCopy(configContent)}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '0px',
|
||||
right: '5px',
|
||||
color: '#999',
|
||||
transition: 'none',
|
||||
'&.MuiButtonBase-root:hover': {
|
||||
background: 'transparent',
|
||||
color: '#3D46F2',
|
||||
transition: 'none'
|
||||
}
|
||||
}}
|
||||
></IconButton>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default IntegrationAIContainer
|
||||
@@ -0,0 +1,200 @@
|
||||
import InsidePage from '@common/components/aoplatform/InsidePage'
|
||||
import { IconButton } from '@common/components/postcat/api/IconButton'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { Button, Card, App } from 'antd'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import AddMcpKey, { addMcpKeysHandle } from './AddMcpKey'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
|
||||
const McpKeyContainer = () => {
|
||||
const { fetchData } = useFetch()
|
||||
const { message, modal } = App.useApp()
|
||||
const [keys, setKeys] = useState<any[]>([])
|
||||
const [, forceUpdate] = useState<unknown>(null)
|
||||
const { state } = useGlobalContext()
|
||||
const addMcpKeyModalRef = useRef<addMcpKeysHandle>(null)
|
||||
|
||||
/**
|
||||
* 新增 API Key
|
||||
*/
|
||||
const addKey = () => {
|
||||
modal.confirm({
|
||||
title: $t('新增 API Key'),
|
||||
content: <AddMcpKey ref={addMcpKeyModalRef}></AddMcpKey>,
|
||||
onOk: () => {
|
||||
return addMcpKeyModalRef.current?.save().then((res) => {
|
||||
if (res) {
|
||||
getKeysList()
|
||||
}
|
||||
})
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 API Key 列表
|
||||
*/
|
||||
const getKeysList = () => {
|
||||
fetchData<BasicResponse<null>>('system/apikeys', {
|
||||
method: 'GET'
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg, data } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setKeys(data.apikeys || [])
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
message.error(errorInfo || $t(RESPONSE_TIPS.error))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制 API Key
|
||||
*/
|
||||
const copyCode = async (value: string): Promise<void> => {
|
||||
if (value) {
|
||||
await navigator.clipboard.writeText(value)
|
||||
message.success($t(RESPONSE_TIPS.copySuccess))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 API Key
|
||||
*/
|
||||
const deleteKey = (id: string) => {
|
||||
modal.confirm({
|
||||
title: $t('删除'),
|
||||
content: $t('确定删除吗?'),
|
||||
onOk: async () => {
|
||||
try {
|
||||
const response = await fetchData<BasicResponse<'success'>>('system/apikey', {
|
||||
method: 'DELETE',
|
||||
eoParams: { apikey: id }
|
||||
})
|
||||
if (response.code === STATUS_CODE.SUCCESS) {
|
||||
message.success($t('删除成功'))
|
||||
getKeysList()
|
||||
}
|
||||
} catch (error) {
|
||||
message.error($t('删除失败'))
|
||||
}
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑 API Key
|
||||
*/
|
||||
const editKey = (key: any) => {
|
||||
console.log('any', key)
|
||||
modal.confirm({
|
||||
title: $t('编辑'),
|
||||
content: (
|
||||
<AddMcpKey ref={addMcpKeyModalRef} name={key.name} value={key.value} apikey={key.id} type={'edit'}></AddMcpKey>
|
||||
),
|
||||
onOk: () => {
|
||||
return addMcpKeyModalRef.current?.save().then((res) => {
|
||||
if (res) {
|
||||
getKeysList()
|
||||
}
|
||||
})
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getKeysList()
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
forceUpdate({})
|
||||
}, [state.language])
|
||||
return (
|
||||
<>
|
||||
<InsidePage
|
||||
pageTitle={$t('API Key')}
|
||||
description={$t('API 密钥可用于调用系统级 Open API 和 MCP。')}
|
||||
showBorder={false}
|
||||
scrollPage={false}
|
||||
>
|
||||
<Button type="primary" onClick={addKey}>
|
||||
{$t('新增 API Key')}
|
||||
</Button>
|
||||
<div className="api-key-container mt-[20px]">
|
||||
{keys.map((key, index) => (
|
||||
<Card style={{ width: 600, borderRadius: '10px' }} key={index} className="mt-[10px]">
|
||||
<div className="flex">
|
||||
<div className="flex-1">
|
||||
<p className="text-[14px] font-bold">{key.name}</p>
|
||||
<p className="flex">
|
||||
<span className="h-[26px] leading-[28px]">{key.value}</span>
|
||||
<IconButton
|
||||
name="copy"
|
||||
onClick={() => {
|
||||
copyCode(key?.value)
|
||||
}}
|
||||
sx={{
|
||||
color: '#333',
|
||||
transition: 'none',
|
||||
'&.MuiButtonBase-root:hover': {
|
||||
background: 'transparent',
|
||||
color: '#3D46F2',
|
||||
transition: 'none'
|
||||
}
|
||||
}}
|
||||
></IconButton>
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-[30px] flex justify-center items-center">
|
||||
<IconButton
|
||||
name="edit"
|
||||
onClick={() => {
|
||||
editKey(key)
|
||||
}}
|
||||
sx={{
|
||||
color: '#333',
|
||||
transition: 'none',
|
||||
'&.MuiButtonBase-root:hover': { background: 'transparent', color: '#3D46F2', transition: 'none' }
|
||||
}}
|
||||
></IconButton>
|
||||
<IconButton
|
||||
name="delete"
|
||||
onClick={() => {
|
||||
deleteKey(key.id)
|
||||
}}
|
||||
sx={{
|
||||
color: '#333',
|
||||
transition: 'none',
|
||||
'&.MuiButtonBase-root:hover': { background: 'transparent', color: 'red', transition: 'none' }
|
||||
}}
|
||||
></IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</InsidePage>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default McpKeyContainer
|
||||
@@ -0,0 +1,29 @@
|
||||
import InsidePage from "@common/components/aoplatform/InsidePage"
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { Card } from "antd"
|
||||
import IntegrationAIContainer from "./IntegrationAIContainer"
|
||||
|
||||
const McpServiceContainer = () => {
|
||||
const handleApiKeyChange = (value: string) => {
|
||||
console.log(value)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<InsidePage
|
||||
pageTitle={$t('MCP 服务')}
|
||||
description={$t('MCP Service 充当 AI 模型与 API 之间的桥梁,允许智能助手(如 Claude)动态发现和调用 Gateway 上的 API,无需繁琐的手动配置或自定义集成。')}
|
||||
showBorder={false}
|
||||
scrollPage={false}
|
||||
>
|
||||
<div className="flex mt-[10px] pr-[40px]">
|
||||
<Card style={{ borderRadius: '10px' }} className="flex-1 w-[400px] mr-[10px]">
|
||||
444
|
||||
</Card>
|
||||
<IntegrationAIContainer type={'global'} handleApiKeyChange={handleApiKeyChange}></IntegrationAIContainer>
|
||||
</div>
|
||||
</InsidePage>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default McpServiceContainer
|
||||
@@ -0,0 +1,33 @@
|
||||
// import { InspectorConfig } from "./configurationTypes";
|
||||
|
||||
// OAuth-related session storage keys
|
||||
export const SESSION_KEYS = {
|
||||
CODE_VERIFIER: "mcp_code_verifier",
|
||||
SERVER_URL: "mcp_server_url",
|
||||
TOKENS: "mcp_tokens",
|
||||
CLIENT_INFORMATION: "mcp_client_information",
|
||||
} as const;
|
||||
|
||||
export type ConnectionStatus =
|
||||
| "disconnected"
|
||||
| "connected"
|
||||
| "error"
|
||||
| "error-connecting-to-proxy";
|
||||
|
||||
export const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277";
|
||||
|
||||
/**
|
||||
* Default configuration for the MCP Inspector, Currently persisted in local_storage in the Browser.
|
||||
* Future plans: Provide json config file + Browser local_storage to override default values
|
||||
**/
|
||||
export const DEFAULT_INSPECTOR_CONFIG: any = {
|
||||
MCP_SERVER_REQUEST_TIMEOUT: {
|
||||
description: "Timeout for requests to the MCP server (ms)",
|
||||
value: 10000,
|
||||
},
|
||||
MCP_PROXY_FULL_ADDRESS: {
|
||||
description:
|
||||
"Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
|
||||
value: "",
|
||||
},
|
||||
} as const;
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
NotificationSchema as BaseNotificationSchema,
|
||||
ClientNotificationSchema,
|
||||
ServerNotificationSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export const StdErrNotificationSchema = BaseNotificationSchema.extend({
|
||||
method: z.literal("notifications/stderr"),
|
||||
params: z.object({
|
||||
content: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const NotificationSchema = ClientNotificationSchema.or(
|
||||
StdErrNotificationSchema,
|
||||
)
|
||||
.or(ServerNotificationSchema)
|
||||
.or(BaseNotificationSchema);
|
||||
|
||||
export type StdErrNotification = z.infer<typeof StdErrNotificationSchema>;
|
||||
export type Notification = z.infer<typeof NotificationSchema>;
|
||||
@@ -0,0 +1,382 @@
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import {
|
||||
SSEClientTransport,
|
||||
SseError,
|
||||
} from "@modelcontextprotocol/sdk/client/sse.js";
|
||||
import { App } from 'antd'
|
||||
import {
|
||||
ClientNotification,
|
||||
ClientRequest,
|
||||
CreateMessageRequestSchema,
|
||||
ListRootsRequestSchema,
|
||||
ProgressNotificationSchema,
|
||||
ResourceUpdatedNotificationSchema,
|
||||
LoggingMessageNotificationSchema,
|
||||
Request,
|
||||
Result,
|
||||
ServerCapabilities,
|
||||
PromptReference,
|
||||
ResourceReference,
|
||||
McpError,
|
||||
CompleteResultSchema,
|
||||
ErrorCode,
|
||||
CancelledNotificationSchema,
|
||||
ResourceListChangedNotificationSchema,
|
||||
ToolListChangedNotificationSchema,
|
||||
PromptListChangedNotificationSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { useState } from "react";
|
||||
import { z } from "zod";
|
||||
import { ConnectionStatus, SESSION_KEYS } from "./constants";
|
||||
import { Notification, StdErrNotificationSchema } from "./notificationTypes";
|
||||
// import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
|
||||
// import { authProvider } from "../auth";
|
||||
// import packageJson from "../../../package.json";
|
||||
|
||||
|
||||
interface UseConnectionOptions {
|
||||
transportType: "stdio" | "sse";
|
||||
command?: string;
|
||||
args?: string;
|
||||
sseUrl: string;
|
||||
env?: Record<string, string>;
|
||||
proxyServerUrl: string;
|
||||
bearerToken?: string;
|
||||
requestTimeout?: number;
|
||||
onNotification?: (notification: Notification) => void;
|
||||
onStdErrNotification?: (notification: Notification) => void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onPendingRequest?: (request: any, resolve: any, reject: any) => void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getRoots?: () => any[];
|
||||
}
|
||||
|
||||
interface RequestOptions {
|
||||
signal?: AbortSignal;
|
||||
timeout?: number;
|
||||
suppressToast?: boolean;
|
||||
}
|
||||
|
||||
export function useConnection({
|
||||
transportType,
|
||||
command,
|
||||
args,
|
||||
sseUrl,
|
||||
env,
|
||||
proxyServerUrl,
|
||||
bearerToken,
|
||||
requestTimeout,
|
||||
onNotification,
|
||||
onStdErrNotification,
|
||||
onPendingRequest,
|
||||
getRoots,
|
||||
}: UseConnectionOptions) {
|
||||
const [connectionStatus, setConnectionStatus] =
|
||||
useState<ConnectionStatus>("disconnected");
|
||||
const { message } = App.useApp()
|
||||
const [serverCapabilities, setServerCapabilities] =
|
||||
useState<ServerCapabilities | null>(null);
|
||||
const [mcpClient, setMcpClient] = useState<Client | null>(null);
|
||||
const [requestHistory, setRequestHistory] = useState<
|
||||
{ request: string; response?: string }[]
|
||||
>([]);
|
||||
const [completionsSupported, setCompletionsSupported] = useState(true);
|
||||
|
||||
const pushHistory = (request: object, response?: object) => {
|
||||
setRequestHistory((prev) => [
|
||||
...prev,
|
||||
{
|
||||
request: JSON.stringify(request),
|
||||
response: response !== undefined ? JSON.stringify(response) : undefined,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const makeRequest = async <T extends z.ZodType>(
|
||||
request: ClientRequest,
|
||||
schema: T,
|
||||
options?: RequestOptions,
|
||||
): Promise<z.output<T>> => {
|
||||
if (!mcpClient) {
|
||||
throw new Error("MCP client not connected");
|
||||
}
|
||||
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
const timeoutId = setTimeout(() => {
|
||||
abortController.abort("Request timed out");
|
||||
}, options?.timeout ?? requestTimeout);
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await mcpClient.request(request, schema, {
|
||||
signal: options?.signal ?? abortController.signal,
|
||||
});
|
||||
pushHistory(request, response);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
pushHistory(request, { error: errorMessage });
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (e: unknown) {
|
||||
if (!options?.suppressToast) {
|
||||
const errorString = (e as Error).message ?? String(e);
|
||||
message.error(errorString)
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCompletion = async (
|
||||
ref: ResourceReference | PromptReference,
|
||||
argName: string,
|
||||
value: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<string[]> => {
|
||||
if (!mcpClient || !completionsSupported) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const request: ClientRequest = {
|
||||
method: "completion/complete",
|
||||
params: {
|
||||
argument: {
|
||||
name: argName,
|
||||
value,
|
||||
},
|
||||
ref,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await makeRequest(request, CompleteResultSchema, {
|
||||
signal,
|
||||
suppressToast: true,
|
||||
});
|
||||
return response?.completion.values || [];
|
||||
} catch (e: unknown) {
|
||||
// Disable completions silently if the server doesn't support them.
|
||||
// See https://github.com/modelcontextprotocol/specification/discussions/122
|
||||
if (e instanceof McpError && e.code === ErrorCode.MethodNotFound) {
|
||||
setCompletionsSupported(false);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Unexpected errors - show toast and rethrow
|
||||
message.error(e instanceof Error ? e.message : String(e))
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const sendNotification = async (notification: ClientNotification) => {
|
||||
if (!mcpClient) {
|
||||
const error = new Error("MCP client not connected");
|
||||
message.error(error.message)
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
await mcpClient.notification(notification);
|
||||
// Log successful notifications
|
||||
pushHistory(notification);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof McpError) {
|
||||
// Log MCP protocol errors
|
||||
pushHistory(notification, { error: e.message });
|
||||
}
|
||||
message.error(e instanceof Error ? e.message : String(e))
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
// TODO_先屏蔽,暂时不需要
|
||||
// const checkProxyHealth = async () => {
|
||||
// try {
|
||||
// const proxyHealthUrl = new URL(`${proxyServerUrl}/health`);
|
||||
// const proxyHealthResponse = await fetch(proxyHealthUrl);
|
||||
// const proxyHealth = await proxyHealthResponse.json();
|
||||
// if (proxyHealth?.status !== "ok") {
|
||||
// throw new Error("MCP Proxy Server is not healthy");
|
||||
// }
|
||||
// } catch (e) {
|
||||
// console.error("Couldn't connect to MCP Proxy Server", e);
|
||||
// throw e;
|
||||
// }
|
||||
// };
|
||||
// TODO_先屏蔽,暂时不需要
|
||||
// const handleAuthError = async (error: unknown) => {
|
||||
// if (error instanceof SseError && error.code === 401) {
|
||||
// sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
|
||||
|
||||
// const result = await auth(authProvider, { serverUrl: sseUrl });
|
||||
// return result === "AUTHORIZED";
|
||||
// }
|
||||
|
||||
// return false;
|
||||
// };
|
||||
|
||||
const connect = async (_e?: unknown, retryCount: number = 0) => {
|
||||
const client = new Client<Request, Notification, Result>(
|
||||
{
|
||||
name: "mcp-inspector",
|
||||
version: '0.0.1',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
sampling: {},
|
||||
roots: {
|
||||
listChanged: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
// TODO_暂时不需要
|
||||
// try {
|
||||
// await checkProxyHealth();
|
||||
// } catch {
|
||||
// setConnectionStatus("error-connecting-to-proxy");
|
||||
// return;
|
||||
// }
|
||||
// 使用与http.ts一致的方式处理URL
|
||||
// 注意:proxyServerUrl应该是完整URL,或者我们需要为其添加基础URL
|
||||
// 处理两种情况:完整URL或相对路径
|
||||
let fullUrl;
|
||||
if (proxyServerUrl.startsWith('http://') || proxyServerUrl.startsWith('https://')) {
|
||||
// 如果是完整URL,直接使用
|
||||
fullUrl = `${proxyServerUrl}/sse`;
|
||||
} else {
|
||||
// 如果是相对路径,添加基础URL和API前缀
|
||||
const baseUrl = window.location.origin;
|
||||
const apiPrefix = '/api/v1/';
|
||||
fullUrl = `${baseUrl}${apiPrefix}${proxyServerUrl}`;
|
||||
}
|
||||
const mcpProxyServerUrl = new URL(fullUrl);
|
||||
mcpProxyServerUrl.searchParams.append("transportType", transportType);
|
||||
if (transportType === "stdio") {
|
||||
mcpProxyServerUrl.searchParams.append("command", command || '');
|
||||
mcpProxyServerUrl.searchParams.append("args", args || '');
|
||||
mcpProxyServerUrl.searchParams.append("env", JSON.stringify(env || {}));
|
||||
} else {
|
||||
mcpProxyServerUrl.searchParams.append("url", sseUrl);
|
||||
}
|
||||
console.log('sseUrl===', sseUrl)
|
||||
try {
|
||||
// Inject auth manually instead of using SSEClientTransport, because we're
|
||||
// proxying through the inspector server first.
|
||||
const headers: HeadersInit = {};
|
||||
|
||||
// TODO_暂时不需要。Use manually provided bearer token if available, otherwise use OAuth tokens
|
||||
// const token = bearerToken || (await authProvider.tokens())?.access_token;
|
||||
// if (token) {
|
||||
// headers["Authorization"] = `Bearer ${token}`;
|
||||
// }
|
||||
|
||||
// 创建SSE客户端传输层
|
||||
const clientTransport = new SSEClientTransport(mcpProxyServerUrl, {
|
||||
eventSourceInit: {
|
||||
fetch: (url, init) => fetch(url, { ...init, headers }),
|
||||
},
|
||||
requestInit: {
|
||||
headers,
|
||||
},
|
||||
});
|
||||
// TODO_暂时不需要
|
||||
// if (onNotification) {
|
||||
// [
|
||||
// CancelledNotificationSchema,
|
||||
// ProgressNotificationSchema,
|
||||
// LoggingMessageNotificationSchema,
|
||||
// ResourceUpdatedNotificationSchema,
|
||||
// ResourceListChangedNotificationSchema,
|
||||
// ToolListChangedNotificationSchema,
|
||||
// PromptListChangedNotificationSchema,
|
||||
// ].forEach((notificationSchema) => {
|
||||
// client.setNotificationHandler(notificationSchema, onNotification);
|
||||
// });
|
||||
|
||||
// client.fallbackNotificationHandler = (
|
||||
// notification: Notification,
|
||||
// ): Promise<void> => {
|
||||
// onNotification(notification);
|
||||
// return Promise.resolve();
|
||||
// };
|
||||
// }
|
||||
|
||||
// if (onStdErrNotification) {
|
||||
// client.setNotificationHandler(
|
||||
// StdErrNotificationSchema,
|
||||
// onStdErrNotification,
|
||||
// );
|
||||
// }
|
||||
|
||||
try {
|
||||
await client.connect(clientTransport);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to connect to MCP Server via the MCP Inspector Proxy: ${mcpProxyServerUrl}:`,
|
||||
error,
|
||||
);
|
||||
// TODO_先屏蔽,后续如果需要再处理
|
||||
// const shouldRetry = await handleAuthError(error);
|
||||
// if (shouldRetry) {
|
||||
// return connect(undefined, retryCount + 1);
|
||||
// }
|
||||
|
||||
if (error instanceof SseError && error.code === 401) {
|
||||
// Don't set error state if we're about to redirect for auth
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const capabilities = client.getServerCapabilities();
|
||||
setServerCapabilities(capabilities ?? null);
|
||||
setCompletionsSupported(true); // Reset completions support on new connection
|
||||
// TODO_暂时不需要
|
||||
// if (onPendingRequest) {
|
||||
// client.setRequestHandler(CreateMessageRequestSchema, (request) => {
|
||||
// return new Promise((resolve, reject) => {
|
||||
// onPendingRequest(request, resolve, reject);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// if (getRoots) {
|
||||
// client.setRequestHandler(ListRootsRequestSchema, async () => {
|
||||
// return { roots: getRoots() };
|
||||
// });
|
||||
// }
|
||||
|
||||
setMcpClient(client);
|
||||
setConnectionStatus("connected");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setConnectionStatus("error");
|
||||
}
|
||||
};
|
||||
|
||||
const disconnect = async () => {
|
||||
await mcpClient?.close();
|
||||
setMcpClient(null);
|
||||
setConnectionStatus("disconnected");
|
||||
setCompletionsSupported(false);
|
||||
setServerCapabilities(null);
|
||||
};
|
||||
|
||||
return {
|
||||
connectionStatus,
|
||||
serverCapabilities,
|
||||
mcpClient,
|
||||
requestHistory,
|
||||
makeRequest,
|
||||
sendNotification,
|
||||
handleCompletion,
|
||||
completionsSupported,
|
||||
connect,
|
||||
disconnect,
|
||||
};
|
||||
}
|
||||
@@ -11,10 +11,10 @@ import { normFile } from '@common/utils/uploadPic.ts'
|
||||
import { validateUrlSlash } from '@common/utils/validate.ts'
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
|
||||
import { AiServiceConfigFieldType } from '@core/const/ai-service/type.ts'
|
||||
import { SERVICE_APPROVAL_OPTIONS } from '@core/const/system/const.tsx'
|
||||
import { MCP_OPTIONS, SERVICE_APPROVAL_OPTIONS } from '@core/const/system/const.tsx'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import { CategorizesType } from '@market/const/serviceHub/type.ts'
|
||||
import { App, Button, Form, Input, Radio, Row, Select, Tooltip, TreeSelect, Upload } from 'antd'
|
||||
import { App, Button, Form, Input, Radio, RadioChangeEvent, Row, Select, Switch, Tooltip, TreeSelect, Upload } from 'antd'
|
||||
import { DefaultOptionType } from 'antd/es/cascader'
|
||||
import { RcFile, UploadChangeParam, UploadFile, UploadProps } from 'antd/es/upload/interface'
|
||||
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'
|
||||
@@ -23,6 +23,7 @@ import { v4 as uuidv4 } from 'uuid'
|
||||
import { SystemConfigFieldType, SystemConfigHandle } from '../../const/system/type.ts'
|
||||
import { useSystemContext } from '../../contexts/SystemContext.tsx'
|
||||
import { Codebox } from '@common/components/postcat/api/Codebox/index.tsx'
|
||||
import { useAiServiceContext } from '@core/contexts/AiServiceContext.tsx'
|
||||
|
||||
export type SimpleAiProviderItem = EntityItem & {
|
||||
configured: boolean
|
||||
@@ -39,6 +40,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
const navigate = useNavigate()
|
||||
const { setBreadcrumb } = useBreadcrumb()
|
||||
const { setSystemInfo } = useSystemContext()
|
||||
const { setAiServiceInfo } = useAiServiceContext()
|
||||
const [showClassify, setShowClassify] = useState<boolean>(true)
|
||||
const [showAI, setShowAI] = useState<boolean>(false)
|
||||
const [imageBase64, setImageBase64] = useState<string | null>(null)
|
||||
@@ -274,6 +276,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
setSystemInfo(data.service)
|
||||
setAiServiceInfo(data.service)
|
||||
return Promise.resolve(true)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
@@ -359,10 +362,35 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
form.setFieldValue('team', teamId)
|
||||
form.setFieldValue('serviceType', 'public')
|
||||
form.setFieldValue('approvalType', 'auto')
|
||||
form.setFieldValue('enable_mcp', false)
|
||||
}
|
||||
return form.setFieldsValue({})
|
||||
}, [serviceId])
|
||||
|
||||
const handleMcpChange = (e: RadioChangeEvent) => {
|
||||
if (e.target.value) {
|
||||
return
|
||||
}
|
||||
modal.confirm({
|
||||
title: $t('关闭 MCP'),
|
||||
content: $t('关闭后将无法通过MCP方式调用服务'),
|
||||
onOk: () => {
|
||||
form.setFieldValue('enable_mcp', false)
|
||||
},
|
||||
onCancel: () => {
|
||||
form.setFieldValue('enable_mcp', true)
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
okButtonProps: {
|
||||
danger: true
|
||||
},
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
const deleteSystemModal = async () => {
|
||||
modal.confirm({
|
||||
title: $t('删除'),
|
||||
@@ -387,6 +415,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
() => SERVICE_APPROVAL_OPTIONS.map((x) => ({ ...x, label: $t(x.label) })),
|
||||
[state.language]
|
||||
)
|
||||
const mcpOptions = useMemo(() => MCP_OPTIONS.map((x) => ({ ...x, label: $t(x.label) })), [state.language])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -440,6 +469,9 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item<AiServiceConfigFieldType> label={$t('MCP')} name="enable_mcp" rules={[{ required: true }]}>
|
||||
<Radio.Group className="flex flex-col" options={mcpOptions} onChange={serviceId ? handleMcpChange : undefined}/>
|
||||
</Form.Item>
|
||||
{showAI && (
|
||||
<>
|
||||
<Form.Item<AiServiceConfigFieldType>
|
||||
@@ -450,9 +482,14 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
>
|
||||
{providerOptionList && providerOptionList.length > 0 ? (
|
||||
<Select
|
||||
showSearch
|
||||
className="w-INPUT_NORMAL"
|
||||
filterOption={(input, option) => (option?.searchText ?? '').includes(input.toLowerCase())}
|
||||
placeholder={$t(PLACEHOLDER.input)}
|
||||
options={providerOptionList}
|
||||
options={providerOptionList.map((x) => ({
|
||||
...x,
|
||||
searchText: x.name.toLowerCase()
|
||||
}))}
|
||||
onChange={(e) => {
|
||||
modelProviderChange(e)
|
||||
}}
|
||||
@@ -467,7 +504,20 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item<AiServiceConfigFieldType> label={$t('默认模型')} name="model" rules={[{ required: true }]}>
|
||||
<Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} options={modelList}></Select>
|
||||
<Select
|
||||
showSearch
|
||||
filterOption={(input, option) => (option?.searchText ?? '').includes(input.toLowerCase())}
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.input)}
|
||||
options={
|
||||
modelList
|
||||
? modelList.map((x) => ({
|
||||
...x,
|
||||
searchText: x.name.toLowerCase()
|
||||
}))
|
||||
: []
|
||||
}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -234,6 +234,7 @@ const SystemInsidePage: FC = () => {
|
||||
<InsidePage
|
||||
pageTitle={systemInfo?.name || '-'}
|
||||
tagList={[
|
||||
...(systemInfo?.enable_mcp ? [{ label: 'MCP', color: '#ffc107', className: 'text-[#000]' }] : []),
|
||||
{
|
||||
label: (
|
||||
<Paragraph className="mb-0" copyable={serviceId ? { text: serviceId } : false}>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { App } from 'antd'
|
||||
import { App, Tag } from 'antd'
|
||||
import { FC, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { SERVICE_KIND_OPTIONS, SYSTEM_TABLE_COLUMNS } from '../../const/system/const.tsx'
|
||||
@@ -176,10 +176,16 @@ const SystemList: FC = () => {
|
||||
x.valueEnum = teamList
|
||||
}
|
||||
if ((x.dataIndex as string) === 'service_kind') {
|
||||
x.valueEnum = {}
|
||||
SERVICE_KIND_OPTIONS.forEach((option) => {
|
||||
;(x.valueEnum as any)[option.value] = { text: $t(option.label) }
|
||||
})
|
||||
x.render = (dom: React.ReactNode, record: any) => (
|
||||
<span
|
||||
className={`text-[13px] `}
|
||||
>
|
||||
{$t(SERVICE_KIND_OPTIONS.find((x) => x.value === record.service_kind)?.label || '-')}
|
||||
{record.enable_mcp && (
|
||||
<Tag color="#ffc107" className="text-[#000] ml-[5px]">MCP</Tag>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if ((x.dataIndex as string) === 'state') {
|
||||
x.render = (dom: React.ReactNode, record: any) => (
|
||||
|
||||
@@ -23,12 +23,32 @@ func genOperation(summary string, description string, variables []*ai_api_dto.Ai
|
||||
operation := openapi3.NewOperation()
|
||||
operation.Summary = summary
|
||||
operation.Description = description
|
||||
operation.AddParameter(&openapi3.Parameter{
|
||||
Name: "Authorization",
|
||||
In: "header",
|
||||
Required: true,
|
||||
Example: "{your_apipark_apikey}",
|
||||
})
|
||||
operation.RequestBody = genRequestBody(variables)
|
||||
operation.Responses = &openapi3.Responses{}
|
||||
operation.Responses.Set("200", genResponse())
|
||||
return operation
|
||||
}
|
||||
|
||||
func genRequestHeaders() openapi3.Parameters {
|
||||
return openapi3.Parameters{
|
||||
{
|
||||
Value: &openapi3.Parameter{
|
||||
Name: "Authorization",
|
||||
In: "header",
|
||||
Description: "your_apipark_apikey", // 替换Prompt的变量列表
|
||||
Required: true,
|
||||
Example: "your_apipark_apikey",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func genRequestParameters(variables []*ai_api_dto.AiPromptVariable) openapi3.Parameters {
|
||||
return openapi3.Parameters{
|
||||
{
|
||||
|
||||
@@ -220,7 +220,8 @@ func (i *imlBalanceModule) Delete(ctx context.Context, id string) error {
|
||||
return i.syncGateway(ctx, cluster.DefaultClusterID, []*gateway.DynamicRelease{
|
||||
{
|
||||
BasicItem: &gateway.BasicItem{
|
||||
ID: id,
|
||||
ID: id,
|
||||
Resource: "ai-provider",
|
||||
},
|
||||
},
|
||||
}, false)
|
||||
@@ -267,7 +268,8 @@ func (i *imlBalanceModule) getLocalBalances(ctx context.Context, v string) ([]*g
|
||||
var has bool
|
||||
v, has = i.settingService.Get(ctx, "system.ai_model.ollama_address")
|
||||
if !has {
|
||||
return nil, fmt.Errorf("ollama address not found")
|
||||
//return nil, fmt.Errorf("ollama address not found")
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +295,8 @@ func (i *imlBalanceModule) getBalances(ctx context.Context) ([]*gateway.DynamicR
|
||||
}
|
||||
v, has := i.settingService.Get(ctx, "system.ai_model.ollama_address")
|
||||
if !has {
|
||||
return nil, fmt.Errorf("ollama address not found")
|
||||
//return nil, fmt.Errorf("ollama address not found")
|
||||
return nil, nil
|
||||
}
|
||||
releases := make([]*gateway.DynamicRelease, 0, len(balances))
|
||||
for _, item := range balances {
|
||||
@@ -304,6 +307,10 @@ func (i *imlBalanceModule) getBalances(ctx context.Context) ([]*gateway.DynamicR
|
||||
continue
|
||||
}
|
||||
base = fmt.Sprintf("%s://%s%s", p.URI().Scheme(), p.URI().Host(), p.URI().Path())
|
||||
} else {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
releases = append(releases, newRelease(item, base))
|
||||
}
|
||||
|
||||
@@ -383,12 +383,12 @@ func (i *imlKeyModule) UpdateKeyStatus(ctx context.Context, providerId string, i
|
||||
}
|
||||
releases := []*gateway.DynamicRelease{{
|
||||
BasicItem: &gateway.BasicItem{
|
||||
ID: id,
|
||||
ID: fmt.Sprintf("%s-%s", providerId, id),
|
||||
Resource: "ai-key",
|
||||
},
|
||||
Attr: nil,
|
||||
}}
|
||||
return i.syncGateway(ctx, providerId, releases, false)
|
||||
return i.syncGateway(ctx, cluster.DefaultClusterID, releases, false)
|
||||
}
|
||||
if info.Status == ai_key_dto.KeyDisable.Int() || info.Status == ai_key_dto.KeyExceed.Int() {
|
||||
// 超额 或 停用状态,可启用
|
||||
@@ -411,7 +411,7 @@ func (i *imlKeyModule) UpdateKeyStatus(ctx context.Context, providerId string, i
|
||||
return err
|
||||
}
|
||||
releases := []*gateway.DynamicRelease{newKey(info)}
|
||||
return i.syncGateway(ctx, providerId, releases, true)
|
||||
return i.syncGateway(ctx, cluster.DefaultClusterID, releases, true)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
ai_balance "github.com/APIParkLab/APIPark/service/ai-balance"
|
||||
|
||||
@@ -60,6 +61,7 @@ func (i *imlLocalModel) SyncLocalModels(ctx context.Context, address string) err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return i.syncGateway(ctx, cluster.DefaultClusterID, releases, true)
|
||||
}
|
||||
|
||||
@@ -246,7 +248,7 @@ func (i *imlLocalModel) pullHook(fn ...func() error) func(msg ai_provider_local.
|
||||
v, _ := i.settingService.Get(ctx, "system.ai_model.ollama_address")
|
||||
|
||||
cfg := make(map[string]interface{})
|
||||
cfg["provider"] = "ollama"
|
||||
cfg["provider"] = ai_provider_local.ProviderLocal
|
||||
cfg["model"] = msg.Model
|
||||
cfg["model_config"] = ai_provider_local.LocalConfig
|
||||
cfg["priority"] = 0
|
||||
@@ -448,7 +450,7 @@ func (i *imlLocalModel) Enable(ctx context.Context, model string) error {
|
||||
}
|
||||
v, _ := i.settingService.Get(ctx, "system.ai_model.ollama_address")
|
||||
cfg := make(map[string]interface{})
|
||||
cfg["provider"] = "ollama"
|
||||
cfg["provider"] = ai_provider_local.ProviderLocal
|
||||
cfg["model"] = info.Id
|
||||
cfg["model_config"] = ai_provider_local.LocalConfig
|
||||
cfg["priority"] = 0
|
||||
@@ -512,8 +514,8 @@ func (i *imlLocalModel) OnInit() {
|
||||
})
|
||||
models, version := ai_provider_local.ModelsCanInstall()
|
||||
for _, model := range models {
|
||||
|
||||
if v, ok := oldModels[model.Id]; ok {
|
||||
delete(oldModels, model.Id)
|
||||
if v.Version == version {
|
||||
continue
|
||||
}
|
||||
@@ -541,7 +543,7 @@ func (i *imlLocalModel) OnInit() {
|
||||
return
|
||||
}
|
||||
}
|
||||
delete(oldModels, model.Id)
|
||||
|
||||
}
|
||||
for id := range oldModels {
|
||||
err = i.localModelPackageService.Delete(ctx, id)
|
||||
@@ -612,17 +614,18 @@ func (i *imlLocalModel) getLocalModels(ctx context.Context, v string) ([]*gatewa
|
||||
var has bool
|
||||
v, has = i.settingService.Get(ctx, "system.ai_model.ollama_address")
|
||||
if !has {
|
||||
return nil, errors.New("ollama_address not set")
|
||||
//return nil, errors.New("ollama_address not set")
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
provider := ai_provider_local.ProviderLocal
|
||||
releases := make([]*gateway.DynamicRelease, 0, len(list))
|
||||
for _, l := range list {
|
||||
if l.State != ai_local_dto.LocalModelStateNormal.Int() {
|
||||
continue
|
||||
}
|
||||
cfg := make(map[string]interface{})
|
||||
cfg["provider"] = "ollama"
|
||||
cfg["provider"] = provider
|
||||
cfg["model"] = l.Id
|
||||
cfg["model_config"] = ai_provider_local.LocalConfig
|
||||
cfg["base"] = v
|
||||
@@ -639,6 +642,25 @@ func (i *imlLocalModel) getLocalModels(ctx context.Context, v string) ([]*gatewa
|
||||
Attr: cfg,
|
||||
})
|
||||
}
|
||||
releases = append(releases, &gateway.DynamicRelease{
|
||||
BasicItem: &gateway.BasicItem{
|
||||
ID: fmt.Sprintf("%s-key", ai_provider_local.ProviderLocal),
|
||||
Description: "auto generate key",
|
||||
Resource: "ai-key",
|
||||
Version: time.Now().Format("20060102150405"),
|
||||
MatchLabels: map[string]string{
|
||||
"module": "ai-key",
|
||||
},
|
||||
},
|
||||
Attr: map[string]interface{}{
|
||||
"expired": 0,
|
||||
"config": fmt.Sprintf("{\"base\":\"%s\"}", v),
|
||||
"provider": provider,
|
||||
"priority": 1,
|
||||
"disabled": true,
|
||||
},
|
||||
})
|
||||
|
||||
return releases, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ai_model "github.com/APIParkLab/APIPark/service/ai-model"
|
||||
@@ -200,6 +201,10 @@ func (i *imlProviderModule) Delete(ctx context.Context, id string) error {
|
||||
}
|
||||
|
||||
func (i *imlProviderModule) AddProvider(ctx context.Context, input *ai_dto.NewProvider) (*ai_dto.SimpleProvider, error) {
|
||||
_, has := model_runtime.GetProvider(strings.ToLower(input.Name))
|
||||
if has {
|
||||
return nil, fmt.Errorf("provider `%s` duplicate", input.Name)
|
||||
}
|
||||
// uuid = name
|
||||
if has := i.providerService.CheckUuidDuplicate(ctx, input.Name); has {
|
||||
return nil, fmt.Errorf("provider `%s` duplicate", input.Name)
|
||||
@@ -241,7 +246,7 @@ func (i *imlProviderModule) SimpleProvider(ctx context.Context, id string) (*ai_
|
||||
|
||||
func (i *imlProviderModule) ConfiguredProviders(ctx context.Context, keyword string) ([]*ai_dto.ConfiguredProviderItem, error) {
|
||||
// 获取已配置的AI服务商
|
||||
list, err := i.providerService.Search(ctx, keyword, nil, "update_at")
|
||||
list, err := i.providerService.Search(ctx, keyword, nil, "update_at desc")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get provider list error:%v", err)
|
||||
}
|
||||
@@ -694,7 +699,6 @@ func (i *imlProviderModule) UpdateProviderConfig(ctx context.Context, id string,
|
||||
}, newKey(defaultKey),
|
||||
}, true)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -596,9 +596,9 @@ func (i *imlMonitorStatisticModule) TopAPIStatistics(ctx context.Context, limit
|
||||
} else {
|
||||
statisticItem.IsRed = true
|
||||
if key == "-" {
|
||||
statisticItem.Name = "无API"
|
||||
statisticItem.Name = "Unknown API"
|
||||
} else {
|
||||
statisticItem.Name = fmt.Sprintf("未知API-%s", key)
|
||||
statisticItem.Name = fmt.Sprintf("Unknow-%s", key)
|
||||
}
|
||||
}
|
||||
result = append(result, statisticItem)
|
||||
|
||||
@@ -396,15 +396,36 @@ func (i *imlServiceModule) Create(ctx context.Context, teamID string, input *ser
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := i.serviceModelMappingService.Save(ctx, &service_model_mapping.Save{
|
||||
Sid: input.Id,
|
||||
Content: input.ModelMapping,
|
||||
})
|
||||
err := i.serviceService.Create(ctx, mo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return i.serviceService.Create(ctx, mo)
|
||||
if input.ModelMapping != "" {
|
||||
m := make(map[string]string)
|
||||
err = json.Unmarshal([]byte(input.ModelMapping), &m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = i.serviceModelMappingService.Save(ctx, &service_model_mapping.Save{
|
||||
Sid: input.Id,
|
||||
Content: input.ModelMapping,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := i.clusterService.GatewayClient(ctx, cluster.DefaultClusterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = client.Hash().Online(ctx, &gateway.HashRelease{
|
||||
HashKey: fmt.Sprintf("%s:%s", gateway.KeyServiceMapping, input.Id),
|
||||
HashMap: m,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -473,7 +494,7 @@ func (i *imlServiceModule) Edit(ctx context.Context, id string, input *service_d
|
||||
|
||||
}
|
||||
}
|
||||
if input.ModelMapping != nil {
|
||||
if input.ModelMapping != nil && *input.ModelMapping != "" {
|
||||
m := make(map[string]string)
|
||||
err = json.Unmarshal([]byte(*input.ModelMapping), &m)
|
||||
if err != nil {
|
||||
|
||||
@@ -76,6 +76,7 @@ func FromEntity(e *api.AiAPIInfo) *API {
|
||||
Updater: e.Updater,
|
||||
Disable: e.Disable,
|
||||
UseToken: e.UseToken,
|
||||
Provider: e.Provider,
|
||||
Type: e.Type,
|
||||
AdditionalConfig: cfg,
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ type imlLocalModelService struct {
|
||||
}
|
||||
|
||||
func (i *imlLocalModelService) UpdateProvider(ctx context.Context, provider string, ids ...string) error {
|
||||
_, err := i.store.UpdateWhere(ctx, map[string]interface{}{"provider": provider}, map[string]interface{}{"uuid": ids})
|
||||
_, err := i.store.UpdateField(ctx, "provider", provider, "uuid in (?)", ids)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +138,8 @@ type LocalModelPackage struct {
|
||||
Name string `gorm:"type:varchar(100);not null;column:name;comment:名称"`
|
||||
Size string `gorm:"type:varchar(100);not null;column:size;comment:模型大小"`
|
||||
Hash string `gorm:"type:varchar(100);not null;column:hash;comment:模型hash"`
|
||||
Description string `gorm:"type:varchar(255);not null;column:description;comment:描述"`
|
||||
Description string `gorm:"type:varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;not null;column:description;comment:描述"`
|
||||
Text string `gorm:"type:varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;not null;column:text;comment:描述"`
|
||||
Version string `gorm:"type:varchar(100);not null;column:version;comment:版本"`
|
||||
IsPopular bool `gorm:"type:tinyint(1);not null;column:is_popular;comment:是否热门"`
|
||||
}
|
||||
@@ -187,4 +188,3 @@ func (i *ProviderModel) TableName() string {
|
||||
func (i *ProviderModel) IdValue() int64 {
|
||||
return i.Id
|
||||
}
|
||||
|
||||
|
||||