Compare commits

..

25 Commits

Author SHA1 Message Date
lcx 1485b31226 fix: Fix department selection issue 2025-08-12 10:43:17 +08:00
lcx 4b8fa43c36 fix: Fix department selection issue. 2025-08-12 10:23:14 +08:00
lcx e928cd84d7 fix: issue with adding user permissions 2025-08-11 18:59:39 +08:00
lcx e91a9e7726 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-23 15:40:13 +08:00
lcx dff6e722c0 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-22 10:45:16 +08:00
lcx 025bd4c6cc feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-22 10:28:59 +08:00
lcx b2a8c8d901 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-22 09:53:41 +08:00
lcx 0e1efc9656 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-22 09:40:30 +08:00
lcx 5898337481 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-21 18:30:50 +08:00
lcx aff2d1ce01 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-21 18:14:12 +08:00
lcx a574ed8dae feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-21 14:59:27 +08:00
lcx 6b90770a92 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 18:04:41 +08:00
lcx 217e31787c feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 17:29:43 +08:00
lcx 6ced12af75 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 16:34:51 +08:00
lcx 9e7feff093 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 16:32:29 +08:00
lcx 707d98ba34 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 15:17:17 +08:00
lcx 78dc382e82 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 14:32:06 +08:00
lcx a03e310418 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 14:16:08 +08:00
lcx e485ae2511 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 13:45:25 +08:00
lcx 526dddb579 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 13:39:44 +08:00
lcx 16e5a37087 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 11:51:36 +08:00
lcx 12c5ac85d3 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 11:50:13 +08:00
lcx 393271d72a feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 11:29:20 +08:00
Dot.L 1bddef2bda Merge pull request #334 from APIParkLab/feature/liujian-1.8
Optimizing the parameter tiling of MCP Tool to facilitate AI understanding
2025-07-09 18:49:29 +08:00
Liujian 58d02bcf08 Optimizing the parameter tiling of MCP Tool to facilitate AI understanding 2025-07-09 18:48:35 +08:00
38 changed files with 1332 additions and 620 deletions
+23
View File
@@ -190,6 +190,7 @@ func (i *imlMcpController) OnComplete() {
i.server["ja-JP"] = server.NewSSEServer(i.generateJPMCPServer(), server.WithBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath)))
i.openServer = server.NewSSEServer(enSer, server.WithBasePath(fmt.Sprintf("/openapi/v1/%s", strings.Trim(mcp_server.GlobalBasePath, "/"))))
}
func (i *imlMcpController) GlobalMCPHandle(ctx *gin.Context) {
@@ -263,6 +264,28 @@ func (i *imlMcpController) ServiceHandleMessage(ctx *gin.Context) {
i.handleMessage(ctx, mcp_server.DefaultMCPServer())
}
func (i *imlMcpController) ServiceHandleStreamHTTP(ctx *gin.Context) {
apikey := ctx.Request.URL.Query().Get("apikey")
serviceId := ctx.Param("serviceId")
if serviceId == "" {
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid service id", "success": "fail"})
return
}
ok, err := i.authorizationModule.CheckAPIKeyAuthorization(ctx, serviceId, apikey)
if err != nil {
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": err.Error(), "success": "fail"})
return
}
if !ok {
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid apikey", "success": "fail"})
return
}
cfg := i.settingModule.Get(ctx)
req := ctx.Request.WithContext(utils.SetGatewayInvoke(ctx.Request.Context(), cfg.InvokeAddress))
req = req.WithContext(utils.SetLabel(req.Context(), "apikey", apikey))
mcp_server.DefaultMCPServer().ServeHTTP(ctx.Writer, req)
}
func (i *imlMcpController) handleMessage(ctx *gin.Context, server http.Handler) {
sessionId := ctx.Request.URL.Query().Get("sessionId")
apikey, ok := i.sessionKeys.Load(sessionId)
+1
View File
@@ -15,6 +15,7 @@ type IMcpController interface {
ServiceHandleSSE(ctx *gin.Context)
ServiceHandleMessage(ctx *gin.Context)
GlobalMCPConfig(ctx *gin.Context) (string, error)
ServiceHandleStreamHTTP(ctx *gin.Context)
}
func init() {
+1 -1
View File
@@ -8,7 +8,7 @@
"description": "",
"scripts": {
"test": "jest",
"build": "set NODE_OPTIONS=--max-old-space-size=4096 && lerna run build --scope=core --stream --verbose ",
"build": "set NODE_OPTIONS=--max-old-space-size=8192 && lerna run build --scope=core --stream --verbose ",
"serve": "lerna run preview --parallel",
"serve:remotes": "lerna run serve --scope=remote --parallel",
"dev": "lerna run dev --scope=core --stream",
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

@@ -137,7 +137,7 @@ function BasicLayout({ project = 'core' }: { project: string }) {
const items: MenuProps['items'] = useMemo(
() =>
[
userInfo?.type !== 'guest' && {
!['guest', 'third-user'].includes(userInfo?.type as string) && {
key: '2',
label: (
<Button
@@ -119,6 +119,11 @@ export const PERMISSION_DEFINITION = [
anyOf: [{ backend: ['system.organization.role.manager_team_role'] }]
}
},
'system.organization.auth.view': {
granted: {
anyOf: [{ backend: ['system.settings.login.manager', 'system.settings.login.view'] }]
}
},
'system.api_market.service_classification.view': {
granted: {
anyOf: [{ backend: ['system.settings.general.view'] }]
@@ -634,6 +639,15 @@ export const PERMISSION_DEFINITION = [
]
}
},
'team.consumer.mcp.view': {
granted: {
anyOf: [
{
backend: ['team.consumer.mcp.manager', 'team.consumer.mcp.view']
}
]
}
},
'team.application.authorization.add': {
granted: {
anyOf: [{ backend: ['system.workspace.application.manager_all', 'team.consumer.authorization.manager'] }]
@@ -207,8 +207,15 @@ const mockData = [
name: '角色',
key: 'role',
path: '/role',
icon: 'ic:baseline-verified-user',
icon: 'ph:user-circle-gear-fill',
access: 'system.organization.role.view'
},
{
name: '鉴权',
key: 'auth',
path: '/auth',
icon: 'ic:baseline-verified-user',
access: 'system.organization.auth.view'
}
]
},
@@ -60,6 +60,16 @@ const mockData = {
}
]
},
{
driver: 'apipark.builtIn.component',
name: 'auth',
router: [
{
path: 'auth',
type: 'normal'
}
]
},
{
driver: 'apipark.builtIn.component',
name: 'cluster',
@@ -461,6 +461,17 @@
"待审核": "K35612f29",
"已审核": "K47eaafde",
"发布申请": "K56b4254f",
"鉴权": "Kb35e6a18",
"系统用户账号登录授权配置": "K679bd7e4",
"授权类型": "K9e7bb257",
"请选择授权类型": "Kc499fc1d",
"APP ID": "Kee9f8f26",
"请输入APP ID": "K9e4c19bb",
"APP ID 参数位于飞书开发人员控制台中的应用程序凭证和基础信息页面上": "K45c99f97",
"APP Secret": "K90f7c3b4",
"请输入APP Secret": "Kdc53d96f",
"APP Secret 参数位于飞书开发人员控制台中的应用程序凭证和基础信息页面上": "K56e77c4",
"启用授权": "K50693bd8",
"API 调用地址": "Kea2f9279",
"API base URL 一般设置为API 网关的外部网络访问地址,或者是API网关绑定的域名。": "K7fc496a1",
"OpenAPI & MCP 调用地址": "Ka7ca8fde",
@@ -553,6 +564,7 @@
"请输入密码": "K25c895d5",
"密码": "K551b0348",
"登录": "Kd2c1a316",
"飞书授权登录": "K682b11cb",
"访客模式": "K192b3e38",
"您可通过访客模式查看所有页面和功能,但是无法编辑数据。访客模式仅用于了解产品功能,您可以在正式产品中关闭该功能。": "K91aa4801",
"Version (0)-(1)": "K480045ce",
@@ -564,6 +576,8 @@
"AI 代理集成": "Ke6908f16",
"请先订阅该服务": "K71ed51fa",
"申请": "K4aa9ed2c",
"未配置 API Key": "Kf7b54a1",
"配置": "K2a1422d2",
"选择 API Key": "K1bec8cbe",
"新增 API Key": "Kb0e0aeda",
"API 密钥可用于调用系统级 Open API 和 MCP。": "K9d81999c",
@@ -972,5 +972,19 @@
"K6c267c7b": "Avg Requests per Subscriber",
"K133d4291": "Avg Traffic per Subscriber",
"K37c5f1d0": "Token",
"Kb98264d4": "Avg Token per Subscriber"
"Kb98264d4": "Avg Token per Subscriber",
"Kb35e6a18": "Auth",
"K679bd7e4": "System User Account Login Authorization Settings",
"K9e7bb257": "Authorization Type",
"Kc499fc1d": "Please select an authorization type",
"Kee9f8f26": "APP ID",
"K9e4c19bb": "Please enter APP ID",
"K90f7c3b4": "APP Secret",
"Kdc53d96f": "Please enter APP Secret",
"K50693bd8": "Enable Authorization",
"K682b11cb": "Feishu Authorization Login",
"K45c99f97": "The APP ID parameter can be found on the App Credentials and Basic Information page in the Feishu Developer Console",
"K56e77c4": "The APP Secret parameter can be found on the App Credentials and Basic Information page in the Feishu Developer Console",
"Kf7b54a1": "API Key not configured",
"K2a1422d2": "Configure"
}
@@ -994,5 +994,19 @@
"K6c267c7b": "消費者あたりの平均リクエスト数",
"K133d4291": "消費者あたりの平均ネットワークトラフィック",
"K37c5f1d0": "トークン消費量",
"Kb98264d4": "消費者あたりの平均トークン消費量"
"Kb98264d4": "消費者あたりの平均トークン消費量",
"Kb35e6a18": "認証",
"K679bd7e4": "システムユーザーアカウントログイン認可設定",
"K9e7bb257": "認可タイプ",
"Kc499fc1d": "認可タイプを選択してください",
"Kee9f8f26": "APP ID",
"K9e4c19bb": "APP ID を入力してください",
"K90f7c3b4": "APP Secret",
"Kdc53d96f": "APP Secret を入力してください",
"K50693bd8": "認可を有効化",
"K682b11cb": "Feishu 認証ログイン",
"K45c99f97": "APP ID パラメータは Feishu 開発者コンソールのアプリ認証情報と基本情報ページにあります",
"K56e77c4": "APP Secret パラメータは Feishu 開発者コンソールのアプリ認証情報と基本情報ページにあります",
"Kf7b54a1": "API Key が設定されていません",
"K2a1422d2": "設定"
}
@@ -923,5 +923,19 @@
"K6c267c7b": "平均每消费者的请求次数",
"K133d4291": "平均每消费者的网络流量",
"K37c5f1d0": "Token 消耗",
"Kb98264d4": "平均每消费者的 Token 消耗"
"Kb98264d4": "平均每消费者的 Token 消耗",
"Kb35e6a18": "鉴权",
"K679bd7e4": "系统用户账号登录授权配置",
"K9e7bb257": "授权类型",
"Kc499fc1d": "请选择授权类型",
"Kee9f8f26": "APP ID",
"K9e4c19bb": "请输入APP ID",
"K90f7c3b4": "APP Secret",
"Kdc53d96f": "请输入APP Secret",
"K50693bd8": "启用授权",
"K682b11cb": "飞书授权登录",
"K45c99f97": "APP ID 参数位于飞书开发人员控制台中的应用程序凭证和基础信息页面上",
"K56e77c4": "APP Secret 参数位于飞书开发人员控制台中的应用程序凭证和基础信息页面上",
"Kf7b54a1": "未配置 API Key",
"K2a1422d2": "配置"
}
@@ -994,5 +994,19 @@
"K6c267c7b": "平均每位使用者的請求次數",
"K133d4291": "平均每位使用者的網路流量",
"K37c5f1d0": "Token 消耗",
"Kb98264d4": "平均每位使用者的 Token 消耗"
"Kb98264d4": "平均每位使用者的 Token 消耗",
"Kb35e6a18": "鑑權",
"K679bd7e4": "系統用戶帳號登入授權配置",
"K9e7bb257": "授權類型",
"Kc499fc1d": "請選擇授權類型",
"Kee9f8f26": "APP ID",
"K9e4c19bb": "請輸入 APP ID",
"K90f7c3b4": "APP Secret",
"Kdc53d96f": "請輸入 APP Secret",
"K50693bd8": "啟用授權",
"K682b11cb": "飛書授權登入",
"K45c99f97": "APP ID 參數位於飛書開發人員控制台中的應用程式憑證與基礎資訊頁面",
"K56e77c4": "APP Secret 參數位於飛書開發人員控制台中的應用程式憑證與基礎資訊頁面",
"Kf7b54a1": "未配置 API Key",
"K2a1422d2": "配置"
}
@@ -618,6 +618,16 @@ export const routerMap: Map<string, RouterMapConfig> = new Map([
/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementAppSetting.tsx'
)
)
},
{
path: 'mcp',
key: 'consumerMcp',
lazy: lazy(
() =>
import(
/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/mcpContent.tsx'
)
)
}
]
},
@@ -693,6 +703,14 @@ export const routerMap: Map<string, RouterMapConfig> = new Map([
]
}
],
[
'auth',
{
type: 'module',
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/auth/Auth.tsx')),
key: 'auth'
}
],
[
'analytics',
{
@@ -19,6 +19,7 @@ export type MemberTableListItem = {
enable:boolean
departmentId:string
roles:EntityItem[]
from: string
};
export type AddToDepartmentProps = {
@@ -40,7 +41,7 @@ export type MemberDropdownModalFieldType = {
export type MemberDropdownModalProps = {
type:'addDep'|'addChild'|'addMember'|'editMember'|'rename'
entity?:(MemberTableListItem & {departmentIds:string[]}) | ({id?:string, departmentIds?:string[],name?:string})
entity?:(MemberTableListItem & {departmentIds:string[]}) | ({id?:string, departmentIds?:string[],name?:string,from?:string})
selectedMemberGroupId?:string
}
+383 -199
View File
@@ -1,221 +1,405 @@
import {FC, useCallback, useEffect, useRef, useState} from "react";
import {App, Button, Divider, Form, FormInstance, Input, Spin, Tooltip} from "antd";
import {useGlobalContext} from "@common/contexts/GlobalStateContext.tsx";
import {useFetch} from "@common/hooks/http.ts";
import {BasicResponse, STATUS_CODE} from "@common/const/const.tsx";
import {useNavigate} from "react-router-dom";
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { App, Button, Divider, Form, FormInstance, Input, Spin, Tooltip } from 'antd'
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
import { useFetch } from '@common/hooks/http.ts'
import { BasicResponse, STATUS_CODE } from '@common/const/const.tsx'
import { useLocation, useNavigate } from 'react-router-dom'
// import {useCrypto} from "../hooks/crypto.ts";
import Logo from '@common/assets/layout-logo.png'
import { $t } from "@common/locales";
import { Icon } from "@iconify/react/dist/iconify.js";
import LanguageSetting from "@common/components/aoplatform/LanguageSetting";
import { LoadingOutlined } from "@ant-design/icons";
import FeishuLogo from '@common/assets/feishu.png'
import { $t } from '@common/locales'
import { Icon } from '@iconify/react/dist/iconify.js'
import LanguageSetting from '@common/components/aoplatform/LanguageSetting'
import { LoadingOutlined } from '@ant-design/icons'
const Login:FC = ()=> {
const {state, dispatch} = useGlobalContext()
const {fetchData} = useFetch()
const { message } = App.useApp()
const navigate = useNavigate();
const formRef = useRef<FormInstance>(null);
const [loading,setLoading] = useState<boolean>()
const [allowGuest, setAllowGuest] = useState<boolean>(false)
const [spinning,setSpinning] = useState<boolean>(false)
const Login: FC = () => {
const { state, dispatch } = useGlobalContext()
const { fetchData } = useFetch()
const { message } = App.useApp()
const navigate = useNavigate()
const formRef = useRef<FormInstance>(null)
const [loading, setLoading] = useState<boolean>()
const [allowGuest, setAllowGuest] = useState<boolean>(false)
const [spinning, setSpinning] = useState<boolean>(false)
// 是否允许飞书登录
const [allowFeishuLogin, setAllowFeishuLogin] = useState<boolean>(false)
// 飞书登录app_id
const [feishuAppId, setFeishuAppId] = useState<string>()
// 获取 url 参数
const query = new URLSearchParams(useLocation().search)
// 是否是飞书登录
const [isFeishuLogin, setIsFeishuLogin] = useState<boolean>(false)
useEffect(() => {
if (isFeishuLogin) {
const callbackUrl = new URLSearchParams(window.location.search).get('callbackUrl')
if (callbackUrl && callbackUrl !== 'null') {
navigate(callbackUrl)
} else {
navigate(state.mainPage)
}
setIsFeishuLogin(false)
}
}, [isFeishuLogin])
/**
* 飞书登录
* @param feishuCode 飞书 code
*/
const feishuLogin = async (feishuCode: string) => {
try {
setLoading(true)
const feishuCallbackUrl = localStorage.getItem('feishuCallbackUrl')
const { code, msg } = await fetchData<BasicResponse<null>>('account/login/feishu', {
method: 'POST',
eoBody: {
code: feishuCode,
redirect_uri: feishuCallbackUrl
}
})
const check = useCallback(()=>{
state.isAuthenticated &&setSpinning(true)
fetchData<BasicResponse<{channel:Array<{name:string}>, status:string}>>('account/login',{method:'GET'}).then(response=>{
const {code,data} = response
if(code === STATUS_CODE.SUCCESS && data.status !== 'anonymous'){
dispatch({type:'LOGIN'})
navigate(state.mainPage,{replace:true})
}else{
dispatch({type:'LOGOUT'})
setAllowGuest(data.channel.filter(x=>x.name === 'guest_access').length > 0)
setSpinning(false)
}
})
},[])
if (code === STATUS_CODE.SUCCESS) {
dispatch({ type: 'LOGIN' })
setIsFeishuLogin(true)
} else {
dispatch({ type: 'LOGOUT' })
setIsFeishuLogin(false)
message.error(msg)
}
} catch (err) {
console.warn(err)
} finally {
setLoading(false)
}
}
const check = useCallback(() => {
state.isAuthenticated && setSpinning(true)
fetchData<BasicResponse<{ channel: Array<{ name: string; config: { [key: string]: any } }>; status: string }>>(
'account/login',
{ method: 'GET' }
).then((response) => {
const { code, data } = response
if (code === STATUS_CODE.SUCCESS && data.status !== 'anonymous') {
dispatch({ type: 'LOGIN' })
navigate(state.mainPage, { replace: true })
} else {
dispatch({ type: 'LOGOUT' })
setAllowGuest(data.channel.filter((x: any) => x.name === 'guest_access').length > 0)
const feishu = data.channel.find((x: any) => x.name === 'feishu')
if (feishu) {
setFeishuAppId(feishu.config.client_id)
setAllowFeishuLogin(true)
}
const code = query.get('code')
if (code) {
feishuLogin(code)
setSpinning(false)
return
}
if (isInFeishuClient() && feishu) {
openFeishuLogin(feishu.config.client_id)
}
setSpinning(false)
}
})
}, [])
const getSystemInfo = useCallback(() => {
fetchData<BasicResponse<{ version: string; buildTime: string }>>('common/version', {
method: 'GET',
eoTransformKeys: ['build_time']
}).then((response) => {
const { code, data } = response
if (code === STATUS_CODE.SUCCESS) {
dispatch({ type: 'UPDATE_VERSION', version: data.version })
dispatch({ type: 'UPDATE_DATE', updateDate: data.buildTime })
}
})
}, [])
const fetchLogin = async (values: any) => {
try {
setLoading(true)
const { username, password } = values
// const encryptedPassword = encryptByEnAES(username, password);
const body = {
name: username,
password: password
// client: 1,
// type: 1,
// app_type: 4,
}
const { code, msg } = await fetchData<BasicResponse<null>>('account/login/username', {
method: 'POST',
eoBody: body
})
if (code === STATUS_CODE.SUCCESS) {
dispatch({ type: 'LOGIN' })
// message.success($t(RESPONSE_TIPS.loginSuccess));
const callbackUrl = new URLSearchParams(window.location.search).get('callbackUrl')
if (callbackUrl && callbackUrl !== 'null') {
navigate(callbackUrl)
} else {
navigate(state.mainPage)
}
} else {
dispatch({ type: 'LOGOUT' })
message.error(msg)
}
} catch (err) {
console.warn(err)
} finally {
setLoading(false)
}
}
const login = async () => {
if (formRef.current) {
const values = await formRef.current.validateFields()
fetchLogin(values)
}
}
const loginAsGuest = () => {
fetchLogin({ username: 'guest', password: '12345678' })
}
const isInFeishuClient = () => {
// 方法1:检查User-Agent
const ua = navigator.userAgent.toLowerCase();
const isLark = ua.includes('lark') || ua.includes('feishu');
const getSystemInfo = useCallback(()=>{
fetchData<BasicResponse<{version:string, buildTime:string}>>('common/version',{method:'GET', eoTransformKeys:['build_time']}).then(response=>{
const {code,data} = response
if(code === STATUS_CODE.SUCCESS){
dispatch({type:'UPDATE_VERSION',version:data.version})
dispatch({type:'UPDATE_DATE',updateDate:data.buildTime})
}
})
},[])
// 方法2:检查全局对象
const hasSDK = typeof window.h5sdk !== 'undefined' || typeof window.tt !== 'undefined';
// 方法3:检查URL参数
const params = new URLSearchParams(window.location.search);
const hasFeishuParams = params.has('from') || params.has('required_launch_ability');
return isLark || hasSDK || hasFeishuParams;
}
// 打开飞书授权页面
const openFeishuLogin = (id?: string) => {
const href = window.location.origin + window.location.pathname
const authUrl = `https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id=${id || feishuAppId}&redirect_uri=${href}`
localStorage.setItem('feishuCallbackUrl', href)
window.location.href = authUrl
}
const fetchLogin = async (values:any)=>{
try {
setLoading(true);
const { username, password } = values;
// const encryptedPassword = encryptByEnAES(username, password);
useEffect(() => {
check()
getSystemInfo()
}, [])
const body = {
name:username,
password: password
// client: 1,
// type: 1,
// app_type: 4,
};
return spinning ? (
<Spin
indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />}
spinning={spinning}
className="w-full h-full flex items-center justify-center"
></Spin>
) : (
<div className="h-full w-full flex flex-col items-center overflow-auto min-h-[490px] bg-[#0d1117]">
<div id="glow-background" className="background-container">
<svg className="background-pattern" aria-hidden="true">
<defs>
<pattern id="pattern-bg" width="200" height="200" patternUnits="userSpaceOnUse">
<path d="M.5 200V.5H200" fill="none"></path>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#pattern-bg)"></rect>
</svg>
const {code,msg } = await fetchData<BasicResponse<null>>('account/login/username',{method:'POST',eoBody:(body)})
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.1"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:svgjs="http://svgjs.dev/svgjs"
viewBox="0 0 800 450"
opacity="1"
>
<defs>
<filter
id="bbblurry-filter"
x="-100%"
y="-100%"
width="400%"
height="400%"
filterUnits="objectBoundingBox"
primitiveUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feGaussianBlur
stdDeviation="99"
x="0%"
y="0%"
width="100%"
height="100%"
in="SourceGraphic"
edgeMode="none"
result="blur"
></feGaussianBlur>
</filter>
</defs>
<g filter="url(#bbblurry-filter)">
<ellipse
rx="80.5"
ry="66.5"
cx="623.0285107902043"
cy="25.708028895006635"
fill="hsla(187, 67%, 50%, 1.00)"
>
<animate
attributeName="fill"
values="hsla(187, 67%, 50%, 1.00); hsla(340, 85%, 60%, 1.00); hsla(60, 90%, 55%, 1.00); hsla(187, 67%, 50%, 1.00)"
dur="6s"
repeatCount="indefinite"
></animate>
</ellipse>
if (code === STATUS_CODE.SUCCESS) {
dispatch({type:'LOGIN'})
// message.success($t(RESPONSE_TIPS.loginSuccess));
const callbackUrl = new URLSearchParams(window.location.search).get('callbackUrl');
if (callbackUrl && callbackUrl !== 'null') {
navigate(callbackUrl);
} else {
navigate(state.mainPage);
}
}else{
dispatch({type:'LOGOUT'})
message.error(msg)
}
<ellipse
rx="80.5"
ry="66.5"
cx="446.471435546875"
cy="-11.694503784179688"
fill="hsla(234, 78%, 61%, 1.00)"
>
<animate
attributeName="fill"
values="hsla(234, 78%, 61%, 1.00); hsla(100, 75%, 60%, 1.00); hsla(290, 80%, 70%, 1.00); hsla(234, 78%, 61%, 1.00)"
dur="8s"
repeatCount="indefinite"
></animate>
</ellipse>
} catch (err) {
console.warn(err);
} finally {
setLoading(false)
}
}
const login = async () => {
if (formRef.current) {
const values = await formRef.current.validateFields();
fetchLogin(values);
}
};
<ellipse
rx="80.5"
ry="66.5"
cx="200.54574247724838"
cy="-19.02454901710908"
fill="hsla(167, 87%, 56%, 1.00)"
>
<animate
attributeName="fill"
values="hsla(167, 87%, 56%, 1.00); hsla(10, 90%, 65%, 1.00); hsla(300, 85%, 50%, 1.00); hsla(167, 87%, 56%, 1.00)"
dur="10s"
repeatCount="indefinite"
></animate>
</ellipse>
const loginAsGuest = ()=>{
fetchLogin({username:'guest',password:'12345678'})
}
<ellipse rx="80.5" ry="66.5" cx="340.05827594708103" cy="-9.424536458161867" fill="hsl(25, 100%, 64%)">
<animate
attributeName="fill"
values="hsl(25, 100%, 64%); hsl(200, 100%, 70%); hsl(50, 95%, 55%); hsl(25, 100%, 64%)"
dur="8s"
repeatCount="indefinite"
></animate>
</ellipse>
</g>
</svg>
</div>
{/* <div className="w-full border-box text-right pr-[40px]"></div> */}
<div className="mx-auto flex-1 flex flex-col items-center justify-center z-[3]">
<div className="mx-auto">
<span className="flex items-center justify-center">
<img className="h-[40px] mr-[8px]" src={Logo} />
</span>
</div>
useEffect(() => {
check()
getSystemInfo()
}, []);
<section className="block w-[410px] mx-auto mt-[46px] p-[30px] box-border rounded-[10px] shadow-[0_5px_20px_0_rgba(0,0,0,5%)] login-block">
<div className="h-full">
<div className="">
<Form onFinish={login} className="w-[350px]" ref={formRef}>
<Form.Item
className="p-0 bg-transparent rounded border-none"
name="username"
rules={[{ required: true, message: $t('请输入账号'), whitespace: true }]}
>
<Input
className="w-[350px] h-[40px] login-input"
placeholder={$t('账号')}
autoComplete="on"
autoFocus
/>
</Form.Item>
return (
spinning?
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={spinning} className='w-full h-full flex items-center justify-center'></Spin> :
<div className="h-full w-full flex flex-col items-center overflow-auto min-h-[490px] bg-[#0d1117]">
<div id="glow-background" className="background-container">
<svg className="background-pattern" aria-hidden="true">
<defs>
<pattern id="pattern-bg" width="200" height="200" patternUnits="userSpaceOnUse">
<path d="M.5 200V.5H200" fill="none"></path>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#pattern-bg)"></rect>
</svg>
<Form.Item
className="p-0 bg-transparent rounded border-none "
name="password"
rules={[{ required: true, message: $t('请输入密码') }]}
>
<Input.Password
className="w-[350px] h-[40px] login-input"
placeholder={$t('密码')}
autoComplete="off"
/>
</Form.Item>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" viewBox="0 0 800 450" opacity="1">
<defs>
<filter id="bbblurry-filter" x="-100%" y="-100%" width="400%" height="400%" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feGaussianBlur stdDeviation="99" x="0%" y="0%" width="100%" height="100%" in="SourceGraphic" edgeMode="none" result="blur"></feGaussianBlur>
</filter>
</defs>
<g filter="url(#bbblurry-filter)">
<ellipse rx="80.5" ry="66.5" cx="623.0285107902043" cy="25.708028895006635" fill="hsla(187, 67%, 50%, 1.00)">
<animate attributeName="fill" values="hsla(187, 67%, 50%, 1.00); hsla(340, 85%, 60%, 1.00); hsla(60, 90%, 55%, 1.00); hsla(187, 67%, 50%, 1.00)" dur="6s" repeatCount="indefinite"></animate>
</ellipse>
<ellipse rx="80.5" ry="66.5" cx="446.471435546875" cy="-11.694503784179688" fill="hsla(234, 78%, 61%, 1.00)">
<animate attributeName="fill" values="hsla(234, 78%, 61%, 1.00); hsla(100, 75%, 60%, 1.00); hsla(290, 80%, 70%, 1.00); hsla(234, 78%, 61%, 1.00)" dur="8s" repeatCount="indefinite"></animate>
</ellipse>
<ellipse rx="80.5" ry="66.5" cx="200.54574247724838" cy="-19.02454901710908" fill="hsla(167, 87%, 56%, 1.00)">
<animate attributeName="fill" values="hsla(167, 87%, 56%, 1.00); hsla(10, 90%, 65%, 1.00); hsla(300, 85%, 50%, 1.00); hsla(167, 87%, 56%, 1.00)" dur="10s" repeatCount="indefinite"></animate>
</ellipse>
<ellipse rx="80.5" ry="66.5" cx="340.05827594708103" cy="-9.424536458161867" fill="hsl(25, 100%, 64%)">
<animate attributeName="fill" values="hsl(25, 100%, 64%); hsl(200, 100%, 70%); hsl(50, 95%, 55%); hsl(25, 100%, 64%)" dur="8s" repeatCount="indefinite"></animate>
</ellipse>
</g>
</svg>
<Form.Item className="p-0 bg-transparent rounded border-none ">
<Button
loading={loading}
className="h-[40px] mt-mbase w-full inline-flex justify-center items-center"
type="primary"
htmlType="submit"
>
{$t('登录')}
</Button>
</Form.Item>
{allowFeishuLogin && (
<>
<Divider />
<Form.Item className="p-0 bg-transparent rounded border-none mb-0">
<Button
loading={loading}
className="h-[40px] w-full inline-flex justify-center items-center"
type="default"
onClick={() => openFeishuLogin(feishuAppId)}
>
<img className="h-[30px]" src={FeishuLogo} />
{$t('飞书授权登录')}
</Button>
</Form.Item>
</>
)}
{allowGuest && (
<>
<Divider />
<Form.Item className="p-0 bg-transparent rounded border-none mb-0">
<Button
loading={loading}
className="h-[40px] w-full inline-flex justify-center items-center"
type="default"
onClick={loginAsGuest}
>
{$t('访客模式')}{' '}
<Tooltip
title={$t(
'您可通过访客模式查看所有页面和功能,但是无法编辑数据。访客模式仅用于了解产品功能,您可以在正式产品中关闭该功能。'
)}
>
<Icon icon="ic:baseline-help" height={18} width={18} />
</Tooltip>
</Button>
</Form.Item>
</>
)}
</Form>
</div>
{/* <div className="w-full border-box text-right pr-[40px]"></div> */}
<div className="mx-auto flex-1 flex flex-col items-center justify-center z-[3]" >
<div className="mx-auto">
<span className="flex items-center justify-center">
<img
className="h-[40px] mr-[8px]"
src={Logo}
/>
</span>
</div>
</div>
</section>
<section className="block w-[410px] mx-auto mt-[46px] p-[30px] box-border rounded-[10px] shadow-[0_5px_20px_0_rgba(0,0,0,5%)] login-block">
<div className="h-full">
<div className="">
<Form onFinish={login} className="w-[350px]"
ref={formRef}>
<Form.Item
className="p-0 bg-transparent rounded border-none"
name="username"
rules={[{ required: true, message: $t('请输入账号') ,whitespace:true }]}
>
<Input
className="w-[350px] h-[40px] login-input"
placeholder={$t("账号")}
autoComplete="on"
autoFocus
/>
</Form.Item>
<Form.Item
className="p-0 bg-transparent rounded border-none "
name="password"
rules={[{ required: true, message: $t('请输入密码') }]}
>
<Input.Password
className="w-[350px] h-[40px] login-input"
placeholder={$t("密码")}
autoComplete="off"
/>
</Form.Item>
<Form.Item
className="p-0 bg-transparent rounded border-none "
>
<Button loading={loading} className="h-[40px] mt-mbase w-full inline-flex justify-center items-center" type="primary" htmlType="submit">
{$t('登录')}
</Button>
</Form.Item>
{
allowGuest && <>
<Divider />
<Form.Item
className="p-0 bg-transparent rounded border-none mb-0"
>
<Button loading={loading} className="h-[40px] w-full inline-flex justify-center items-center" type="default" onClick={loginAsGuest}>
{$t('访客模式')} <Tooltip title={$t('您可通过访客模式查看所有页面和功能,但是无法编辑数据。访客模式仅用于了解产品功能,您可以在正式产品中关闭该功能。')}><Icon icon="ic:baseline-help" height={18} width={18} /></Tooltip>
</Button>
</Form.Item>
</>
}
</Form>
</div>
</div>
</section>
<section className="flex flex-col items-center mt-[46px] text-SECOND_TEXT">
<p className="leading-[28px]">
{$t('Version (0)-(1)',[state?.version,state?.updateDate])}, {$t(state?.powered || '-')}
</p>
<LanguageSetting mode="light"/>
</section>
</div>
</div>
);
<section className="flex flex-col items-center mt-[46px] text-SECOND_TEXT">
<p className="leading-[28px]">
{$t('Version (0)-(1)', [state?.version, state?.updateDate])}, {$t(state?.powered || '-')}
</p>
<LanguageSetting mode="light" />
</section>
</div>
</div>
)
}
export default Login;
export default Login
@@ -0,0 +1,171 @@
import InsidePage from '@common/components/aoplatform/InsidePage'
import WithPermission from '@common/components/aoplatform/WithPermission'
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { useFetch } from '@common/hooks/http'
import { $t } from '@common/locales'
import { App, Button, Form, Input, Row, Select, Switch } from 'antd'
import { useEffect, useState } from 'react'
type AuthSetting = {
config: {
clientId: string
clientSecret: string
}
enabled: boolean
}
type AuthFieldType = {
authType: string
clientId: string
clientSecret: string
enabled: boolean
}
const Auth = () => {
const { message } = App.useApp()
const [form] = Form.useForm()
const { fetchData } = useFetch()
const [, forceUpdate] = useState<unknown>(null)
const { state } = useGlobalContext()
const [thirdPartyDrivers, setThirdPartyDrivers] = useState<{ label: string; value: string }[]>([])
useEffect(() => {
forceUpdate({})
}, [state.language])
const onFinish = () => {
form.validateFields().then((value) => {
return fetchData<BasicResponse<null>>(`account/third/${value.authType}`, {
method: 'POST',
eoBody: {
enable: value.enabled,
config: {
client_id: value.clientId,
client_secret: value.clientSecret
}
}
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
return Promise.resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => {
return Promise.reject(errorInfo)
})
})
}
/**
* 获取第三方授权列表
*/
const getThirdPartyAuthList = () => {
fetchData<
BasicResponse<{
drivers: {
name: string
value: string
}[]
}>
>('account/third', {
method: 'GET',
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setThirdPartyDrivers(data.drivers.map((item: any) => ({ label: item.name, value: item.value })))
if (data.drivers.length) {
form.setFieldValue('authType', data.drivers[0].value)
getThirdPartyAuthSetting()
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
/**
* 获取第三方授权配置
*/
const getThirdPartyAuthSetting = () => {
fetchData<BasicResponse<{ info: AuthSetting }>>(`account/third/${form.getFieldValue('authType')}`, {
method: 'GET',
eoTransformKeys: ['client_id', 'client_secret']
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
form.setFieldsValue({
clientId: data.driver?.config?.clientId || '',
clientSecret: data.driver?.config?.clientSecret || '',
enabled: data.driver?.enable || false
})
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
useEffect(() => {
getThirdPartyAuthList()
}, [])
return (
<InsidePage pageTitle={$t('鉴权')} showBorder={false} contentClassName="pr-PAGE_INSIDE_X" scrollPage={false} description={$t("系统用户账号登录授权配置")}>
<WithPermission access="">
<Form
layout="vertical"
labelAlign="left"
scrollToFirstError
form={form}
className={`mx-auto`}
name="authConfig"
onFinish={onFinish}
autoComplete="off"
>
<Form.Item<AuthFieldType>
label={$t('授权类型')}
name="authType"
rules={[{ required: true, message: $t('请选择授权类型') }]}
>
<Select className="w-INPUT_NORMAL" placeholder={$t('请选择授权类型')} onChange={getThirdPartyAuthSetting} options={thirdPartyDrivers} />
</Form.Item>
<Form.Item<AuthFieldType>
label={$t('APP ID')}
name="clientId"
rules={[{ required: true, whitespace: true, message: $t('请输入APP ID') }]}
extra={$t('APP ID 参数位于飞书开发人员控制台中的应用程序凭证和基础信息页面上')}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} autoComplete="off" />
</Form.Item>
<Form.Item<AuthFieldType>
label={$t('APP Secret')}
name="clientSecret"
rules={[{ required: true, whitespace: true, message: $t('请输入APP Secret') }]}
extra={$t('APP Secret 参数位于飞书开发人员控制台中的应用程序凭证和基础信息页面上')}
>
<Input.Password className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} autoComplete="new-password" />
</Form.Item>
<Form.Item<AuthFieldType> label={$t('启用授权')} name="enabled" valuePropName="checked">
<Switch checkedChildren={$t('启用')} unCheckedChildren={$t('停用')} />
</Form.Item>
<Row className="mb-[10px]">
<WithPermission access="system.devops.system_setting.edit">
<Button type="primary" htmlType="submit">
{$t('保存')}
</Button>
</WithPermission>
</Row>
</Form>
</WithPermission>
</InsidePage>
)
}
export default Auth
@@ -50,17 +50,23 @@ type ServiceApiKeyList = {
expired: number
}>
}
type ConsumerParamsType = {
consumerId: string
teamId: string
}
export interface IntegrationAIContainerRef {
getServiceKeysList: () => void;
}
export interface IntegrationAIContainerProps {
type: 'global' | 'service'
type: 'global' | 'service' | 'consumer'
handleToolsChange: (value: Tool[]) => void
customClassName?: string
service?: ServiceDetailType
serviceId?: string
currentTab?: string
openModal?: (type: 'apply') => void
consumerParams?: ConsumerParamsType
}
export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, IntegrationAIContainerProps>(
({
@@ -69,8 +75,9 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
customClassName,
service,
serviceId,
currentTab,
openModal
currentTab,
openModal,
consumerParams
}: IntegrationAIContainerProps, ref) => {
/** 当前激活的标签 */
const [activeTab, setActiveTab] = useState(type === 'service' ? 'openApi' : 'mcp')
@@ -180,7 +187,35 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
}
})
.catch((errorInfo) => {
message.error(errorInfo || $t(RESPONSE_TIPS.error))
message.error(errorInfo?.toString() || $t(RESPONSE_TIPS.error))
})
}
/**
* 获取消费者 MCP 配置
* @returns
*/
const getConsumerMcpConfig = () => {
fetchData<BasicResponse<null>>('app/mcp/config', {
method: 'GET',
eoParams: { app: consumerParams?.consumerId, team: consumerParams?.teamId }
})
.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?.toString() || $t(RESPONSE_TIPS.error))
})
}
@@ -191,6 +226,10 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
navigator('/mcpKey')
}
const dropAuthPage = () => {
navigator(`/consumer/${consumerParams?.teamId}/inside/${consumerParams?.consumerId}/authorization`)
}
/**
* 获取全局 API Key 列表
*/
@@ -217,7 +256,7 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
}
})
.catch((errorInfo) => {
message.error(errorInfo || $t(RESPONSE_TIPS.error))
message.error(errorInfo?.toString() || $t(RESPONSE_TIPS.error))
})
}
@@ -229,12 +268,12 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
}))
/**
* 获取服务 API Key 列表
* 获取 API Key 列表
*/
const getServiceKeysList = () => {
const getServiceKeysList = (consumerId?: string) => {
fetchData<BasicResponse<null>>(`my/app/apikeys`, {
method: 'GET',
eoParams: { service: serviceId }
eoParams: consumerId ? { app: consumerId } : { service: serviceId }
})
.then((response) => {
const { code, msg, data } = response
@@ -258,7 +297,7 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
}
})
.catch((errorInfo) => {
message.error(errorInfo || $t(RESPONSE_TIPS.error))
message.error(errorInfo?.toString() || $t(RESPONSE_TIPS.error))
})
}
@@ -345,6 +384,10 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
getGlobalMcpConfig()
setMcpServerUrl('mcp/global/sse')
getGlobalKeysList()
} else if (type === 'consumer'){
getConsumerMcpConfig()
setMcpServerUrl(`mcp/app/${consumerParams?.consumerId}/sse`)
getServiceKeysList(consumerParams?.consumerId)
} else {
service?.basic.enableMcp && setMcpServerUrl(`mcp/service/${serviceId}/sse`)
getServiceKeysList()
@@ -362,6 +405,7 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
useEffect(() => {
initTabsData()
type === 'global' && getGlobalMcpConfig()
type === 'consumer' && getConsumerMcpConfig()
}, [state.language])
/**
* 切换标签
@@ -408,7 +452,7 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
}
connectMcpServer()
}
}, [mcpServerUrl, ...(type === 'global' ? [state.language] : [])])
}, [mcpServerUrl, ...(type === 'global' || type === 'consumer' ? [state.language] : [])])
/**
* 获取 MCP tools
*/
@@ -452,7 +496,7 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
</div>
</div>
)}
{type === 'service' && !apiKeyList.length ? (
{(type === 'service' || type === 'consumer') && !apiKeyList.length ? (
<>
<Card
style={{ borderRadius: '10px' }}
@@ -461,12 +505,23 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
body: 'p-[10px]'
}}
>
<div className="flex flex-col items-center justify-center py-3">
<span className="text-[14px] mb-5">{$t('请先订阅该服务')}</span>
<Button type="primary" onClick={() => openModal?.('apply')}>
{$t('申请')}
</Button>
</div>
{
type === 'service' ? (
<div className="flex flex-col items-center justify-center py-3">
<span className="text-[14px] mb-5">{$t('请先订阅该服务')}</span>
<Button type="primary" onClick={() => openModal?.('apply')}>
{$t('申请')}
</Button>
</div>
) : (
<div className="flex flex-col items-center justify-center py-3">
<span className="text-[14px] mb-5">{$t('未配置 API Key')}</span>
<Button type="primary" onClick={() => dropAuthPage()}>
{$t('配置')}
</Button>
</div>
)
}
</Card>
</>
) : (
@@ -14,7 +14,7 @@ export const MemberDropdownModal = forwardRef<MemberDropdownModalHandle,MemberDr
const {fetchData} = useFetch()
const [departmentList, setDepartmentList] = useState<DepartmentListItem[]>([])
const { state } = useGlobalContext()
const [disableEditMemberData] = useState<boolean>(entity?.from === 'feishu')
const save:()=>Promise<boolean | string> = ()=>{
let url:string
let method:string
@@ -182,27 +182,28 @@ export const MemberDropdownModal = forwardRef<MemberDropdownModalHandle,MemberDr
name="name"
rules={[{required: true,whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" disabled={type ==='editMember'} placeholder={$t(PLACEHOLDER.input)}/>
<Input className="w-INPUT_NORMAL" disabled={disableEditMemberData || type ==='editMember'} placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<MemberDropdownModalFieldType>
label={$t("邮箱")}
name="email"
rules={[{required: true,whitespace:true },{type:"email",message: $t(VALIDATE_MESSAGE.email)}]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
<Input className="w-INPUT_NORMAL" disabled={disableEditMemberData} placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<MemberDropdownModalFieldType>
label={$t("密码")}
name="password"
rules={[{required: type === 'addMember',whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
<Input disabled={disableEditMemberData} className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<MemberDropdownModalFieldType>
label={$t("部门")}
name="departmentIds"
>
<TreeSelect
disabled={disableEditMemberData}
className="w-INPUT_NORMAL"
fieldNames={{label:'name',value:'id',children:'children'}}
showSearch
@@ -95,14 +95,15 @@ const AddToDepartment = forwardRef<AddToDepartmentHandle, AddToDepartmentProps>(
treeData?.map((x: DataNode) => ({
...x,
name: $t((x as unknown as { name: string }).name),
checkable: false,
children: x.children?.map(y => ({ ...y, checkable: false }))
checkable: false, // 根节点不可选中
children: x.children?.map(y => ({ ...y, checkable: true })) // 子节点可以选中
})),
[state.language, treeData]
)
const onCheck: TreeProps['onCheck'] = (checkedKeys: string[]) => {
setSelectedKeys(checkedKeys.checked)
const onCheck: TreeProps['onCheck'] = (checkedKeys, info) => {
const selectedIds = Array.isArray(checkedKeys) ? checkedKeys : checkedKeys.checked || []
setSelectedKeys(selectedIds)
}
useEffect(() => {
@@ -153,11 +154,12 @@ const MemberList = () => {
const [tableHttpReload, setTableHttpReload] = useState(true)
const [tableListDataSource, setTableListDataSource] = useState<MemberTableListItem[]>([])
const pageListRef = useRef<ActionType>(null)
const { topGroupId, selectedDepartmentIds, refreshGroup } = useOutletContext<{
const { topGroupId, selectedDepartmentIds, refreshGroup, refreshTableCount } = useOutletContext<{
topGroupId: string
departmentList: DepartmentListItem[]
selectedDepartmentIds: string[]
refreshGroup: () => void
refreshTableCount: number
}>()
const AddMemberRef = useRef<MemberDropdownModalHandle>(null)
const EditMemberRef = useRef<MemberDropdownModalHandle>(null)
@@ -396,7 +398,7 @@ const MemberList = () => {
width: 600,
okText: $t('确认'),
okButtonProps: {
disabled: isActionAllowed(type)
disabled: isActionAllowed(type) || (type === 'editMember' && entity?.from === 'feishu')
},
cancelText: $t('取消'),
closable: true,
@@ -415,6 +417,13 @@ const MemberList = () => {
getDepartmentList()
}, [])
// 监听外部刷新触发器
useEffect(() => {
if (refreshTableCount > 0) {
manualReloadTable()
}
}, [refreshTableCount])
const getDepartmentList = async () => {
setDepartmentValueEnum([])
const { code, data, msg } = await fetchData<BasicResponse<{ department: DepartmentListItem }>>(
@@ -36,6 +36,11 @@ const MemberPage = ()=>{
const [selectedDepartmentId, setSelectedDepartmentId] = useState<string>('-1')
const {accessData,state} = useGlobalContext()
const [refreshMemberCount, setRefreshMemberCount] = useState<number>(0)
const [refreshTableCount, setRefreshTableCount] = useState<number>(0)
const refreshMemberTable = () => {
setRefreshTableCount(prev => prev + 1)
}
const onSearchWordChange = (e:string)=>{
setSearchWord(e || '')
}
@@ -90,7 +95,7 @@ const MemberPage = ()=>{
case 'addChild':
return AddChildRef.current?.save().then((res)=>{if(res === true)getDepartmentList()})
case 'addMember':
return AddMemberRef.current?.save().then((res)=>{if(res === true){getDepartmentList();setRefreshMemberCount(pre=>pre+1)}})
return AddMemberRef.current?.save().then((res)=>{if(res === true){getDepartmentList();setRefreshMemberCount(pre=>pre+1);refreshMemberTable()}})
case 'rename':
return RenameRef.current?.save().then((res)=>{if(res === true)getDepartmentList()})
case 'delete':
@@ -262,7 +267,7 @@ const MemberPage = ()=>{
</div>
</div>
<div className="flex-1 p-btnbase pr-PAGE_INSIDE_X overflow-x-hidden">
<Outlet context={{refreshMemberCount, selectedDepartmentIds,refreshGroup:()=>getDepartmentList()}}/>
<Outlet context={{refreshMemberCount, selectedDepartmentIds,refreshGroup:()=>getDepartmentList(), refreshTableCount}}/>
</div>
</div>
</InsidePage>);
@@ -20,7 +20,7 @@ export default function ManagementInsidePage() {
const { message } = App.useApp()
const { fetchData } = useFetch()
const { setBreadcrumb } = useBreadcrumb()
const [activeMenu, setActiveMenu] = useState<string>('service')
const [activeMenu, setActiveMenu] = useState<string>('authorization')
const { appId, teamId } = useParams<RouterParams>()
const navigateTo = useNavigate()
const currentUrl = useLocation().pathname
@@ -31,8 +31,9 @@ export default function ManagementInsidePage() {
const TENANT_MANAGEMENT_APP_MENU: MenuProps['items'] = useMemo(
() => [
getItem($t('订阅的服务'), 'service', undefined, undefined, undefined, 'team.application.subscription.view'),
getItem($t('访问授权'), 'authorization', undefined, undefined, undefined, 'team.consumer.authorization.view'),
getItem($t('MCP 服务'), 'mcp', undefined, undefined, undefined, 'team.consumer.mcp.view'),
getItem($t('订阅的服务'), 'service', undefined, undefined, undefined, 'team.application.subscription.view'),
getItem($t('消费者管理'), 'setting', undefined, undefined, undefined, 'team.application.application.view')
],
[state.language]
@@ -55,7 +56,7 @@ export default function ManagementInsidePage() {
}, [accessData, accessInit, TENANT_MANAGEMENT_APP_MENU])
useEffect(() => {
setActiveMenu(currentUrl.split('/').pop() || 'service')
setActiveMenu(currentUrl.split('/').pop() || 'authorization')
}, [currentUrl])
const onMenuClick: MenuProps['onClick'] = (node) => {
@@ -336,7 +336,7 @@ export default function ServiceHubManagement() {
setTableSearchWord={setTableSearchWord}
editApp={(row: ServiceHubAppListItem) => {
setAppName(row.name)
navigateTo(`/consumer/${row.team.id}/inside/${row.id}/service`)
navigateTo(`/consumer/${row.team.id}/inside/${row.id}/authorization`)
}}
/>
)}
@@ -0,0 +1,40 @@
import { $t } from '@common/locales'
import { IntegrationAIContainer } from '@core/pages/mcpService/IntegrationAIContainer'
import { Tool } from '@modelcontextprotocol/sdk/types.js'
import { useEffect, useState } from 'react'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import McpToolsContainer from '@core/pages/mcpService/McpToolsContainer'
import { useParams } from 'react-router-dom'
import { RouterParams } from '@common/const/type'
const mcpContent = () => {
const [tools, setTools] = useState<Tool[]>([])
const [, forceUpdate] = useState<unknown>(null)
const { teamId, appId } = useParams<RouterParams>()
const { state } = useGlobalContext()
const handleToolsChange = (value: Tool[]) => {
setTools(value)
}
useEffect(() => {
forceUpdate({})
}, [state.language])
return (
<div className=" h-full pt-[32px]">
<div className="flex items-center justify-between w-full ml-[10px] text-[18px] leading-[25px] pb-[16px]">
<span className="font-bold">{$t('MCP 服务')}</span>
</div>
<div className="h-[calc(100%-41px)] flex flex-col ">
<div className="flex mt-[10px] pr-[40px]">
<McpToolsContainer tools={tools} />
<IntegrationAIContainer
consumerParams={{ consumerId: appId!, teamId: teamId! }}
type={'consumer'}
handleToolsChange={handleToolsChange}
></IntegrationAIContainer>
</div>
</div>
</div>
)
}
export default mcpContent
+5 -3
View File
@@ -9,13 +9,13 @@ require (
github.com/eolinker/eosc v0.18.3
github.com/eolinker/go-common v1.1.7
github.com/gabriel-vasile/mimetype v1.4.4
github.com/getkin/kin-openapi v0.127.0
github.com/getkin/kin-openapi v0.132.0
github.com/gin-contrib/gzip v1.0.1
github.com/gin-gonic/gin v1.10.0
github.com/go-sql-driver/mysql v1.7.0
github.com/google/uuid v1.6.0
github.com/influxdata/influxdb-client-go/v2 v2.14.0
github.com/mark3labs/mcp-go v0.17.0
github.com/mark3labs/mcp-go v0.33.0
github.com/mitchellh/mapstructure v1.5.0
github.com/nsqio/go-nsq v1.1.0
github.com/ollama/ollama v0.5.8
@@ -46,7 +46,6 @@ require (
github.com/golang/snappy v0.0.4 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
github.com/invopop/yaml v0.3.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
@@ -59,10 +58,13 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/oapi-codegen/runtime v1.0.0 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/redis/go-redis/v9 v9.5.3 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
+12 -6
View File
@@ -34,10 +34,12 @@ github.com/eolinker/eosc v0.18.3 h1:3IK5HkAPnJRfLbQ0FR7kWsZr6Y/OiqqGazvN1q2BL5A=
github.com/eolinker/eosc v0.18.3/go.mod h1:O9PQQXFCpB6fjHf+oFt/LN6EOAv779ItbMixMKCfTfk=
github.com/eolinker/go-common v1.1.7 h1:bi7wDmlCYQGjS3k8Bz/o+Mo9aMJAzmPsBLXWurxPfwk=
github.com/eolinker/go-common v1.1.7/go.mod h1:Kb/jENMN1mApnodvRgV4YwO9FJby1Jkt2EUjrBjvSX4=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY=
github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM=
github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk=
github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE=
@@ -78,8 +80,6 @@ github.com/influxdata/influxdb-client-go/v2 v2.14.0 h1:AjbBfJuq+QoaXNcrova8smSjw
github.com/influxdata/influxdb-client-go/v2 v2.14.0/go.mod h1:Ahpm3QXKMJslpXl3IftVLVezreAUtBOTZssDrjZEFHI=
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU=
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@@ -101,8 +101,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mark3labs/mcp-go v0.17.0 h1:5Ps6T7qXr7De/2QTqs9h6BKeZ/qdeUeGrgM5lPzi930=
github.com/mark3labs/mcp-go v0.17.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
github.com/mark3labs/mcp-go v0.33.0 h1:naxhjnTIs/tyPZmWUZFuG0lDmdA6sUyYGGf3gsHvTCc=
github.com/mark3labs/mcp-go v0.33.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
@@ -118,6 +118,10 @@ github.com/nsqio/go-nsq v1.1.0 h1:PQg+xxiUjA7V+TLdXw7nVrJ5Jbl3sN86EhGCQj4+FYE=
github.com/nsqio/go-nsq v1.1.0/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY=
github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo=
github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/ollama/ollama v0.5.8 h1:b2S6YdZ18/ntCsWzoy/HmB3BHGW4GX0Qp7RARrJtJXU=
github.com/ollama/ollama v0.5.8/go.mod h1:ibdmDvb/TjKY1OArBWIazL3pd1DHTk8eG2MMjEkWhiI=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
@@ -134,6 +138,8 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+10 -10
View File
@@ -33,12 +33,12 @@ func TestLoki(t *testing.T) {
// headers["Content-Type"] = "application/json"
// headers["X-Scope-OrgID"] = "tenant1"
// queries := url.Values{}
// queries.Set("query", "{cluster=\"apinto\"} | json | request_id = `c9f6b19c-7dfe-496b-9b39-4d049232fe95`")
// queries.SetMCPServer("query", "{cluster=\"apinto\"} | json | request_id = `c9f6b19c-7dfe-496b-9b39-4d049232fe95`")
// now := time.Now()
// start := now.Add(-time.Hour * 24 * 30)
// queries.Set("start", strconv.FormatInt(start.UnixNano(), 10))
// queries.Set("end", strconv.FormatInt(now.UnixNano(), 10))
// queries.Set("limit", "100")
// queries.SetMCPServer("start", strconv.FormatInt(start.UnixNano(), 10))
// queries.SetMCPServer("end", strconv.FormatInt(now.UnixNano(), 10))
// queries.SetMCPServer("limit", "100")
// a := time.Now()
// result, err := send[LogInfo](http.MethodGet, "http://localhost:3100/loki/api/v1/query_range", headers, queries, "")
// if err != nil {
@@ -57,8 +57,8 @@ func TestLoki(t *testing.T) {
// headers["Content-Type"] = "application/json"
// headers["X-Scope-OrgID"] = "tenant1"
// queries := url.Values{}
// //queries.Set("query", "sum(count_over_time({cluster=\"apinto\"}[24h])) by (strategy)")
// queries.Set("query", "sum(count_over_time({cluster=\"apinto\"}[24h]))")
// //queries.SetMCPServer("query", "sum(count_over_time({cluster=\"apinto\"}[24h])) by (strategy)")
// queries.SetMCPServer("query", "sum(count_over_time({cluster=\"apinto\"}[24h]))")
// result, err := send[LogCount](http.MethodGet, "http://localhost:3100/loki/api/v1/query", headers, queries, "")
// if err != nil {
// t.Fatalf("failed to send request: %v", err)
@@ -75,12 +75,12 @@ func TestLoki(t *testing.T) {
// headers["Content-Type"] = "application/json"
// headers["X-Scope-OrgID"] = "tenant1"
// queries := url.Values{}
// queries.Set("query", "{cluster=\"apinto\"} | json | strategy=\"03899736-5d79-4f26-bd6a-c312a5880780\"")
// queries.SetMCPServer("query", "{cluster=\"apinto\"} | json | strategy=\"03899736-5d79-4f26-bd6a-c312a5880780\"")
// now := time.Now()
// start := now.Add(-time.Hour * 24 * 30)
// queries.Set("start", strconv.FormatInt(start.UnixNano(), 10))
// queries.Set("end", strconv.FormatInt(now.UnixNano(), 10))
// queries.Set("limit", "1")
// queries.SetMCPServer("start", strconv.FormatInt(start.UnixNano(), 10))
// queries.SetMCPServer("end", strconv.FormatInt(now.UnixNano(), 10))
// queries.SetMCPServer("limit", "1")
// now = time.Now()
// result, err := send[map[string]interface{}](http.MethodGet, "http://localhost:3100/loki/api/v1/query_range", headers, queries, "")
// t.LogItem(time.Now().Sub(now))
+87
View File
@@ -0,0 +1,87 @@
package mcp_server
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
)
var client = http.Client{}
type Position string
const (
PositionHeader Position = "header"
PositionBody Position = "body"
PositionQuery Position = "query"
PositionPath Position = "path"
)
type ContentType string
const (
ContentTypeJSON ContentType = "application/json"
ContentTypeXML ContentType = "application/xml"
ContentTypeHTML ContentType = "text/html"
ContentTypeText ContentType = "text/plain"
ContentTypeForm ContentType = "application/x-www-form-urlencoded"
ContentTypeFile ContentType = "multipart/form-data"
)
func NewParam(position Position, required bool, description string) *Param {
return &Param{position: position, required: required, description: description}
}
type Param struct {
position Position
required bool
description string
}
func (p *Param) Description() string {
return p.description
}
func (p *Param) Required() bool {
return p.required
}
type BodyParam struct {
contentType ContentType
params map[string]interface{}
}
func NewBodyParam(contentType string) *BodyParam {
t := ContentType(contentType)
if t == "" {
t = ContentTypeJSON
}
return &BodyParam{contentType: t}
}
func (p *BodyParam) Set(k string, v interface{}) {
if p.params == nil {
p.params = make(map[string]interface{})
}
p.params[k] = v
}
func (p *BodyParam) Encode() (string, error) {
switch p.contentType {
case ContentTypeJSON:
data, err := json.Marshal(p.params)
if err != nil {
return "", fmt.Errorf("body param encode error: %w", err)
}
return string(data), nil
case ContentTypeForm, ContentTypeFile:
data := url.Values{}
for k, v := range p.params {
data.Set(k, fmt.Sprintf("%v", v))
}
return data.Encode(), nil
default:
return "", fmt.Errorf("unsupported content type: %s", p.contentType)
}
}
+213 -34
View File
@@ -4,10 +4,12 @@ import (
"fmt"
"net/http"
"strings"
"sync"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mitchellh/mapstructure"
"github.com/mark3labs/mcp-go/server"
"github.com/eolinker/eosc"
)
var (
@@ -18,55 +20,120 @@ var (
func NewServer() *Server {
return &Server{
sseServers: eosc.BuildUntyped[string, *server.SSEServer](),
servers: make(map[string]*Handler),
}
}
type Server struct {
sseServers eosc.Untyped[string, *server.SSEServer]
servers map[string]*Handler
locker sync.RWMutex
}
func (s *Server) Set(path string, sseServer *server.SSEServer) {
s.sseServers.Set(path, sseServer)
type Handler struct {
*server.MCPServer
handlers map[string]http.Handler
}
func (s *Server) Del(path string) {
s.sseServers.Del(path)
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
sseServer, has := s.sseServers.Get(trimPath(r.URL.Path))
if has {
sseServer.ServeHTTP(w, r)
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r.URL.Path = strings.TrimSuffix(r.URL.Path, "/")
if strings.HasSuffix(r.URL.Path, "/mcp") {
h.handlers["openapi-stream"].ServeHTTP(w, r)
return
}
if strings.HasPrefix(r.URL.Path, "/api") {
h.handlers["api-sse"].ServeHTTP(w, r)
return
} else if strings.HasPrefix(r.URL.Path, "/openapi") {
h.handlers["openapi-sse"].ServeHTTP(w, r)
return
}
http.NotFound(w, r)
return
}
func trimPath(path string) string {
path = strings.TrimSuffix(path, "/")
path = strings.TrimSuffix(path, "/message")
path = strings.TrimSuffix(path, "/sse")
return path
}
func SetSSEServer(sid string, name string, version string, tools ...ITool) {
s := server.NewMCPServer(name, version)
for _, tool := range tools {
tool.RegisterMCP(s)
func (s *Server) Set(id string, ser *server.MCPServer) {
s.locker.Lock()
defer s.locker.Unlock()
tmp := &Handler{
MCPServer: ser,
handlers: make(map[string]http.Handler),
}
apiPath := fmt.Sprintf("/api/v1/%s/%s", ServiceBasePath, sid)
openAPIPath := fmt.Sprintf("/openapi/v1/%s/%s", ServiceBasePath, sid)
mcpServer.Set(apiPath, server.NewSSEServer(s, server.WithBasePath(apiPath)))
mcpServer.Set(openAPIPath, server.NewSSEServer(s, server.WithBasePath(openAPIPath)))
tmp.handlers["api-sse"] = server.NewSSEServer(ser, server.WithStaticBasePath(fmt.Sprintf("/api/v1/%s/%s", ServiceBasePath, id)))
tmp.handlers["openapi-sse"] = server.NewSSEServer(ser, server.WithStaticBasePath(fmt.Sprintf("/openapi/v1/%s/%s", ServiceBasePath, id)))
tmp.handlers["openapi-stream"] = server.NewStreamableHTTPServer(ser, server.WithEndpointPath(fmt.Sprintf("/openapi/v1/%s/%s/mcp", ServiceBasePath, id)))
s.servers[id] = tmp
}
func DelSSEServer(sid string) {
apiPath := fmt.Sprintf("/api/v1/%s/%s", ServiceBasePath, sid)
openAPIPath := fmt.Sprintf("/openapi/v1/%s/%s", ServiceBasePath, sid)
mcpServer.Del(apiPath)
mcpServer.Del(openAPIPath)
func (s *Server) Del(id string) {
s.locker.Lock()
defer s.locker.Unlock()
delete(s.servers, id)
}
func (s *Server) Get(id string) (*Handler, bool) {
s.locker.RLock()
defer s.locker.RUnlock()
ser, has := s.servers[id]
if !has {
return nil, false
}
m := &Handler{
MCPServer: ser.MCPServer,
handlers: make(map[string]http.Handler),
}
for k, v := range ser.handlers {
m.handlers[k] = v
}
return m, true
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
sid, err := genPath(r.URL.Path)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
ser, has := s.Get(sid)
if has {
ser.ServeHTTP(w, r)
return
}
http.NotFound(w, r)
return
}
func genPath(path string) (sid string, err error) {
path = strings.TrimSuffix(path, "/")
ps := strings.Split(path, "/")
if len(ps) < 2 {
err = fmt.Errorf("invalid path: %s", path)
return
}
sid = ps[len(ps)-2]
return
}
func SetServer(sid string, name string, version string, tools ...ITool) {
ser, has := mcpServer.Get(sid)
if !has {
mcpServer.Set(sid, server.NewMCPServer(name, version, server.WithToolCapabilities(true)))
ser, has = mcpServer.Get(sid)
if !has {
return
}
}
ts := make([]server.ServerTool, 0, len(tools))
for _, tool := range tools {
ts = append(ts, tool.Tool())
}
ser.SetTools(ts...)
}
func DelServer(sid string) {
mcpServer.Del(sid)
}
func ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -76,3 +143,115 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request) {
func DefaultMCPServer() *Server {
return mcpServer
}
func SetServerByOpenapi(sid, name, version, content string) error {
mcpInfo, err := ConvertMCPFromOpenAPI3Data([]byte(content))
if err != nil {
return fmt.Errorf("convert mcp from openapi3 data error: %w", err)
}
tools := make([]ITool, 0, len(mcpInfo.Apis))
for _, a := range mcpInfo.Apis {
toolOptions := make([]mcp.ToolOption, 0, len(a.Params)+2)
toolOptions = append(toolOptions, mcp.WithDescription(a.Description))
params := make(map[string]*Param)
for _, v := range a.Params {
params[v.Name] = NewParam(Position(v.In), v.Required, v.Description)
options := make([]mcp.PropertyOption, 0, 2)
if v.Required {
options = append(options, mcp.Required())
}
options = append(options, mcp.Description(v.Description))
toolOptions = append(toolOptions, mcp.WithString(v.Name, options...))
}
if a.Body != nil {
type Schema struct {
Type string `mapstructure:"type"`
Properties map[string]interface{} `mapstructure:"properties"`
Items interface{} `mapstructure:"items"`
Required interface{} `mapstructure:"required"`
}
var tmp Schema
err = mapstructure.Decode(a.Body, &tmp)
if err != nil {
return err
}
required := map[string]struct{}{}
switch t := tmp.Required.(type) {
case []interface{}:
for _, v := range t {
i, ok := v.(string)
if !ok {
continue
}
required[i] = struct{}{}
}
}
for k, v := range tmp.Properties {
description := ""
typ := "string"
isRequired := false
if _, ok := required[k]; ok {
isRequired = true
}
var props map[string]interface{}
var items interface{}
switch t := v.(type) {
case map[string]interface{}:
if m, ok := t["type"]; ok {
n, ok := m.(string)
if ok {
typ = n
}
}
if m, ok := t["description"]; ok {
n, ok := m.(string)
if ok {
description = n
}
}
switch typ {
case "array":
if m, ok := t["items"]; ok {
items = m
}
case "object":
if m, ok := t["properties"]; ok {
n, ok := m.(map[string]interface{})
if ok {
props = n
}
}
}
}
params[k] = NewParam(PositionBody, isRequired, description)
options := make([]mcp.PropertyOption, 0, 3)
options = append(options, mcp.Description(description))
if props != nil {
options = append(options, mcp.Properties(props))
}
if items != nil {
options = append(options, mcp.Items(items))
}
switch typ {
case "string":
toolOptions = append(toolOptions, mcp.WithString(k, options...))
case "integer", "number", "float":
toolOptions = append(toolOptions, mcp.WithNumber(k, options...))
case "boolean":
toolOptions = append(toolOptions, mcp.WithBoolean(k, options...))
case "array":
toolOptions = append(toolOptions, mcp.WithArray(k, options...))
case "object":
toolOptions = append(toolOptions, mcp.WithObject(k, options...))
default:
return fmt.Errorf("unsupported type: %s", typ)
}
}
}
tools = append(tools, NewTool(a.Summary, a.Path, a.Method, a.ContentType, params, toolOptions...))
}
SetServer(sid, name, version, tools...)
return nil
}
+44 -60
View File
@@ -2,7 +2,6 @@ package mcp_server
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
@@ -16,36 +15,38 @@ import (
)
type ITool interface {
RegisterMCP(s *server.MCPServer)
Tool() server.ServerTool
}
const (
MCPBody = "Body"
MCPHeader = "Header"
MCPQuery = "Query"
MCPPath = "Path"
)
type Tool struct {
name string
url string
method string
contentType string
params map[string]*Param
opts []mcp.ToolOption
}
func NewTool(name string, uri string, method string, contentType string, opts ...mcp.ToolOption) ITool {
func (t *Tool) Tool() server.ServerTool {
return server.ServerTool{
Tool: mcp.NewTool(t.name, t.opts...),
Handler: generateInvokeTool(t.url, t.method, t.contentType, t.params),
}
}
func NewTool(name string, uri string, method string, contentType string, params map[string]*Param, opts ...mcp.ToolOption) ITool {
return &Tool{
name: name,
url: uri,
method: method,
contentType: contentType,
params: params,
opts: opts,
}
}
func (t *Tool) RegisterMCP(s *server.MCPServer) {
s.AddTool(mcp.NewTool(t.name, t.opts...), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
func generateInvokeTool(path string, method string, contentType string, params map[string]*Param) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
invokeAddress := utils.GatewayInvoke(ctx)
if invokeAddress == "" {
return nil, fmt.Errorf("invoke address is empty")
@@ -58,69 +59,54 @@ func (t *Tool) RegisterMCP(s *server.MCPServer) {
u.Scheme = "http"
}
path := t.url
queries := url.Values{}
headers := make(map[string]string)
body := ""
for k, v := range request.Params.Arguments {
if k == "Body" {
switch a := v.(type) {
case string:
body = a
case map[string]interface{}:
switch t.contentType {
case "application/json":
tmp, _ := json.Marshal(a)
body = string(tmp)
case "application/x-www-form-urlencoded":
bodyValue := url.Values{}
for kk, vv := range a {
bodyValue.Set(kk, fmt.Sprintf("%v", vv))
}
body = bodyValue.Encode()
bodyParam := NewBodyParam(contentType)
for k, p := range params {
vv, ok := request.GetArguments()[k]
if !ok && p.required {
return nil, fmt.Errorf("param %s is required", k)
}
if p.position == PositionHeader || p.position == PositionQuery || p.position == PositionPath {
v, ok := vv.(string)
if !ok || v == "<nil>" {
if p.required {
return nil, fmt.Errorf("param %s is required", k)
}
default:
tmp, _ := json.Marshal(a)
body = string(tmp)
continue
}
continue
}
tmp, ok := v.(map[string]interface{})
if !ok {
continue
}
switch k {
case MCPHeader:
for kk, vv := range tmp {
headers[kk] = fmt.Sprintf("%v", vv)
}
case MCPQuery:
for kk, vv := range tmp {
queries.Set(kk, fmt.Sprintf("%v", vv))
}
case MCPPath:
for kk, vv := range tmp {
p, ok := vv.(string)
if !ok {
return nil, fmt.Errorf("invalid path %s", v)
}
path = strings.Replace(path, fmt.Sprintf("{%s}", kk), p, -1)
switch p.position {
case PositionPath:
path = strings.ReplaceAll(path, "{"+k+"}", fmt.Sprintf("%v", vv))
case PositionQuery:
queries.Set(k, fmt.Sprintf("%v", vv))
case PositionHeader:
headers[k] = fmt.Sprintf("%v", vv)
case PositionBody:
if vv == nil {
continue
}
bodyParam.Set(k, vv)
}
}
bodyData, err := bodyParam.Encode()
if err != nil {
return nil, err
}
u.Path = path
u.RawQuery = queries.Encode()
req, err := http.NewRequest(t.method, u.String(), strings.NewReader(body))
req, err := http.NewRequest(method, u.String(), strings.NewReader(bodyData))
if err != nil {
return nil, err
}
for k, v := range headers {
req.Header.Set(k, v)
}
if t.contentType != "" {
req.Header.Set("Content-Type", t.contentType)
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
apikey := utils.Label(ctx, "apikey")
if apikey != "" {
@@ -141,7 +127,5 @@ func (t *Tool) RegisterMCP(s *server.MCPServer) {
}
return mcp.NewToolResultText(string(d)), nil
})
}
}
var client = http.Client{}
+24 -15
View File
@@ -7,7 +7,7 @@ import (
func genOpenAPI3Template(title string, description string) *openapi3.T {
result := new(openapi3.T)
result.OpenAPI = "3.1.0"
result.OpenAPI = "3.0.1"
result.Info = &openapi3.Info{
Title: title,
Description: description,
@@ -37,6 +37,8 @@ func genOperation(summary string, description string, variables []*ai_api_dto.Ai
func genRequestBody(variables []*ai_api_dto.AiPromptVariable) *openapi3.RequestBodyRef {
requestBody := openapi3.NewRequestBody()
requestBody.Description = "Request body"
requestBody.Required = true
requestBody.Content = openapi3.NewContentWithSchema(genRequestBodySchema(variables), []string{"application/json"})
return &openapi3.RequestBodyRef{
Value: requestBody,
@@ -55,10 +57,14 @@ func genResponse() *openapi3.ResponseRef {
func genRequestBodySchema(variables []*ai_api_dto.AiPromptVariable) *openapi3.Schema {
result := openapi3.NewObjectSchema()
required := make([]string, 0, 2)
required = append(required, "messages")
if len(variables) > 0 {
result.WithProperty("variables", genVariableSchema(variables))
result.WithRequired([]string{"variables", "messages"})
required = append(required, "variables")
}
result.WithRequired(required)
streamSchema := openapi3.NewBoolSchema()
streamSchema.Title = "stream"
streamSchema.Description = "Whether to stream the response"
@@ -129,6 +135,8 @@ func genMessageSchema() *openapi3.Schema {
"role": roleSchema,
"content": contentSchema,
})
result.WithRequired([]string{"role", "content"})
return result
}
@@ -137,20 +145,21 @@ func genMessagesSchema() *openapi3.Schema {
result.Title = "Messages"
result.Description = "Chat Messages"
result.Items = openapi3.NewSchemaRef("#/components/schemas/Message", messageSchema)
result.Required = []string{"content", "role"}
return result
}
func genResponseSchema() *openapi3.Schema {
result := openapi3.NewObjectSchema()
result.Description = "Response from the server"
// 创建 choices 数组
choicesSchema := openapi3.NewArraySchema()
choiceItemSchema := openapi3.NewObjectSchema()
// choice 中的 message 字段
choiceItemSchema.WithPropertyRef("message", messageSchemaRef)
// finish_reason 字段
finishReasonSchema := openapi3.NewStringSchema().WithEnum(
"stop",
@@ -160,41 +169,41 @@ func genResponseSchema() *openapi3.Schema {
"null",
)
choiceItemSchema.WithProperty("finish_reason", finishReasonSchema)
// index 字段
choiceItemSchema.WithProperty("index", openapi3.NewIntegerSchema())
// logprobs 字段,可以为 null
choiceItemSchema.WithProperty("logprobs", openapi3.NewSchema().WithNullable())
choicesSchema.Items = &openapi3.SchemaRef{Value: choiceItemSchema}
result.WithProperty("choices", choicesSchema)
// object 字段
result.WithProperty("object", openapi3.NewStringSchema().WithEnum("chat.completion"))
// usage 字段
usageSchema := openapi3.NewObjectSchema()
usageSchema.WithProperty("prompt_tokens", openapi3.NewIntegerSchema())
usageSchema.WithProperty("completion_tokens", openapi3.NewIntegerSchema())
usageSchema.WithProperty("total_tokens", openapi3.NewIntegerSchema())
// prompt_tokens_details 字段
promptTokensDetailsSchema := openapi3.NewObjectSchema()
promptTokensDetailsSchema.WithProperty("cached_tokens", openapi3.NewIntegerSchema())
usageSchema.WithProperty("prompt_tokens_details", promptTokensDetailsSchema)
result.WithProperty("usage", usageSchema)
// 其他字段
result.WithProperty("created", openapi3.NewIntegerSchema())
result.WithProperty("system_fingerprint", openapi3.NewStringSchema().WithNullable())
result.WithProperty("model", openapi3.NewStringSchema())
result.WithProperty("id", openapi3.NewStringSchema())
// 保留原有的错误字段
result.WithProperty("code", openapi3.NewIntegerSchema())
result.WithProperty("error", openapi3.NewStringSchema())
return result
}
-49
View File
@@ -78,55 +78,6 @@ type imlCatalogueModule struct {
root *Root
}
//func (i *imlCatalogueModule) OnInit() {
// register.Handle(func(v server.Server) {
// ctx := context.Background()
// list, err := i.releaseService.GetRunningList(ctx)
// if err != nil {
// log.Errorf("onInit: get running list failed:%s", err.Error())
// return
// }
// if len(list) < 1 || list[0].APICount > 0 {
// return
// }
// serviceMap := make(map[string]*release.Release)
// serviceIds := make([]string, 0, len(list))
// for _, v := range list {
// if _, ok := serviceMap[v.Service]; !ok {
// serviceMap[v.Service] = v
// serviceIds = append(serviceIds, v.Service)
// }
// }
// if len(serviceIds) < 1 {
// return
// }
// commitIds, err := i.releaseService.GetRunningApiDocCommits(ctx, serviceIds...)
// if err != nil {
// log.Errorf("onInit: get running api doc commits failed:%s", err.Error())
// return
// }
// if len(commitIds) < 1 {
// return
// }
// listCommits, err := i.apiDocService.ListDocCommit(ctx, commitIds...)
// if err != nil {
// log.Error("onInit: list doc commit failed:", err.Error())
// return
// }
// for _, v := range listCommits {
// m, ok := serviceMap[v.Target]
// if !ok {
// continue
// }
//
// i.releaseService.UpdateRelease(ctx, m.UUID, &release.Update{
// APICount: &v.Data.APICount,
// })
// }
// })
//
//}
func (i *imlCatalogueModule) DefaultCatalogue(ctx context.Context) (*catalogue_dto.Catalogue, error) {
catalogues, err := i.catalogueService.List(ctx)
if err != nil {
+72 -73
View File
@@ -10,7 +10,6 @@ import (
"net/url"
"strconv"
"strings"
"time"
"github.com/APIParkLab/APIPark/service/subscribe"
@@ -48,7 +47,7 @@ type imlMcpModule struct {
}
func (i *imlMcpModule) Services(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
keyword, _ := req.Params.Arguments["keyword"].(string)
keyword, _ := req.GetArguments()["keyword"].(string)
list, err := i.serviceService.Search(ctx, keyword, map[string]interface{}{
"as_server": true,
}, "update_at desc")
@@ -116,34 +115,34 @@ func (i *imlMcpModule) Services(ctx context.Context, req mcp.CallToolRequest) (*
}
func (i *imlMcpModule) Apps(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
keyword := req.Params.Arguments["keyword"].(string)
condition := make(map[string]interface{})
condition["as_app"] = true
list, err := i.serviceService.Search(ctx, keyword, condition, "update_at desc")
if err != nil {
return nil, fmt.Errorf("search service error: %w", err)
}
if len(list) == 0 {
list, err = i.serviceService.Search(ctx, "", condition, "update_at desc")
if err != nil {
return nil, fmt.Errorf("search service error: %w", err)
}
}
data, _ := json.Marshal(utils.SliceToSlice(list, func(s *service.Service) *mcp_dto.App {
return &mcp_dto.App{
Id: s.Id,
Name: s.Name,
Description: s.Name,
CreateTime: s.CreateTime,
UpdateTime: s.UpdateTime,
}
}))
return mcp.NewToolResultText(string(data)), nil
}
//func (i *imlMcpModule) Apps(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// keyword := req.GetArguments()["keyword"].(string)
// condition := make(map[string]interface{})
// condition["as_app"] = true
// list, err := i.serviceService.Search(ctx, keyword, condition, "update_at desc")
// if err != nil {
// return nil, fmt.Errorf("search service error: %w", err)
// }
// if len(list) == 0 {
// list, err = i.serviceService.Search(ctx, "", condition, "update_at desc")
// if err != nil {
// return nil, fmt.Errorf("search service error: %w", err)
// }
// }
// data, _ := json.Marshal(utils.SliceToSlice(list, func(s *service.Service) *mcp_dto.App {
// return &mcp_dto.App{
// Id: s.Id,
// Name: s.Name,
// Description: s.Name,
// CreateTime: s.CreateTime,
// UpdateTime: s.UpdateTime,
// }
// }))
// return mcp.NewToolResultText(string(data)), nil
//}
func (i *imlMcpModule) APIs(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
serviceId, _ := req.Params.Arguments["service"].(string)
serviceId, _ := req.GetArguments()["service"].(string)
serviceIds := make([]string, 0, 1)
if serviceId == "" {
serviceIds = append(serviceIds, serviceId)
@@ -190,45 +189,45 @@ func (i *imlMcpModule) APIs(ctx context.Context, req mcp.CallToolRequest) (*mcp.
return mcp.NewToolResultText(string(data)), nil
}
func (i *imlMcpModule) SubscriberAuthorizations(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
serviceId, ok := req.Params.Arguments["service"].(string)
if !ok {
return nil, fmt.Errorf("service id is required")
}
subscribes, err := i.subscriberService.Subscribers(ctx, serviceId, subscribe.ApplyStatusSubscribe)
if err != nil {
return nil, fmt.Errorf("get subscriber error: %w,service id is %s", err, serviceId)
}
appIds := utils.SliceToSlice(subscribes, func(s *subscribe.Subscribe) string {
return s.Application
})
if len(appIds) == 0 {
return nil, fmt.Errorf("no subscriber found,service id is %s", serviceId)
}
list, err := i.appAuthorizationService.ListByApp(ctx, appIds...)
if err != nil {
return nil, fmt.Errorf("get app authorization error: %w,app ids is %s", err, appIds)
}
result := utils.SliceToSlice(list, func(a *application_authorization.Authorization) *mcp_dto.AppAuthorization {
return &mcp_dto.AppAuthorization{
Id: a.UUID,
Name: a.Name,
Position: a.Position,
TokenName: a.TokenName,
Config: a.Config,
}
}, func(a *application_authorization.Authorization) bool {
if a.Type != "apikey" {
return false
}
if a.ExpireTime != 0 && a.ExpireTime < time.Now().Unix() {
return false
}
return true
})
data, _ := json.Marshal(result)
return mcp.NewToolResultText(string(data)), nil
}
//func (i *imlMcpModule) SubscriberAuthorizations(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// serviceId, ok := req.GetArguments()["service"].(string)
// if !ok {
// return nil, fmt.Errorf("service id is required")
// }
// subscribes, err := i.subscriberService.Subscribers(ctx, serviceId, subscribe.ApplyStatusSubscribe)
// if err != nil {
// return nil, fmt.Errorf("get subscriber error: %w,service id is %s", err, serviceId)
// }
// appIds := utils.SliceToSlice(subscribes, func(s *subscribe.Subscribe) string {
// return s.Application
// })
// if len(appIds) == 0 {
// return nil, fmt.Errorf("no subscriber found,service id is %s", serviceId)
// }
// list, err := i.appAuthorizationService.ListByApp(ctx, appIds...)
// if err != nil {
// return nil, fmt.Errorf("get app authorization error: %w,app ids is %s", err, appIds)
// }
// result := utils.SliceToSlice(list, func(a *application_authorization.Authorization) *mcp_dto.AppAuthorization {
// return &mcp_dto.AppAuthorization{
// Id: a.UUID,
// Name: a.Name,
// position: a.position,
// TokenName: a.TokenName,
// Config: a.Config,
// }
// }, func(a *application_authorization.Authorization) bool {
// if a.Type != "apikey" {
// return false
// }
// if a.ExpireTime != 0 && a.ExpireTime < time.Now().Unix() {
// return false
// }
// return true
// })
// data, _ := json.Marshal(result)
// return mcp.NewToolResultText(string(data)), nil
//}
var (
client = &http.Client{}
@@ -248,18 +247,18 @@ func (i *imlMcpModule) Invoke(ctx context.Context, req mcp.CallToolRequest) (*mc
u.Scheme = "http"
}
path, ok := req.Params.Arguments["path"].(string)
path, ok := req.GetArguments()["path"].(string)
if !ok {
return nil, fmt.Errorf("invalid path")
}
u.Path = fmt.Sprintf("%s/%s", strings.TrimSuffix(u.Path, "/"), strings.TrimPrefix(path, "/"))
method, ok := req.Params.Arguments["method"].(string)
method, ok := req.GetArguments()["method"].(string)
if !ok {
method = "GET"
}
queryParam := url.Values{}
query, ok := req.Params.Arguments["query"].(map[string]interface{})
query, ok := req.GetArguments()["query"].(map[string]interface{})
if ok {
for k, v := range query {
switch v := v.(type) {
@@ -278,7 +277,7 @@ func (i *imlMcpModule) Invoke(ctx context.Context, req mcp.CallToolRequest) (*mc
}
u.RawQuery = queryParam.Encode()
headerParam := http.Header{}
header, ok := req.Params.Arguments["header"].(map[string]interface{})
header, ok := req.GetArguments()["header"].(map[string]interface{})
if ok {
for k, v := range header {
switch v := v.(type) {
@@ -294,12 +293,12 @@ func (i *imlMcpModule) Invoke(ctx context.Context, req mcp.CallToolRequest) (*mc
}
}
body, ok := req.Params.Arguments["body"].(string)
body, ok := req.GetArguments()["body"].(string)
if !ok {
body = ""
}
contentType, ok := req.Params.Arguments["content-type"].(string)
contentType, ok := req.GetArguments()["content-type"].(string)
if !ok {
contentType = "application/json"
}
+2 -2
View File
@@ -13,12 +13,12 @@ type IMcpModule interface {
// Services 获取服务列表
Services(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)
// Apps 获取应用列表
Apps(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)
//Apps(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)
// APIs 获取API列表
APIs(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)
// SubscriberAuthorizations 获取订阅者授权
SubscriberAuthorizations(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)
//SubscriberAuthorizations(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)
Invoke(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)
}
+1 -64
View File
@@ -15,8 +15,6 @@ import (
mcp_server "github.com/APIParkLab/APIPark/mcp-server"
api_doc "github.com/APIParkLab/APIPark/service/api-doc"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mitchellh/mapstructure"
strategy_driver "github.com/APIParkLab/APIPark/module/strategy/driver"
strategy_dto "github.com/APIParkLab/APIPark/module/strategy/dto"
@@ -657,68 +655,7 @@ func (i *imlPublishModule) updateMCPServer(ctx context.Context, sid string, name
if err != nil {
return fmt.Errorf("get api doc commit error: %w", err)
}
mcpInfo, err := mcp_server.ConvertMCPFromOpenAPI3Data([]byte(commitDoc.Data.Content))
if err != nil {
return fmt.Errorf("convert mcp from openapi3 data error: %w", err)
}
tools := make([]mcp_server.ITool, 0, len(mcpInfo.Apis))
for _, a := range mcpInfo.Apis {
toolOptions := make([]mcp.ToolOption, 0, len(a.Params)+2)
toolOptions = append(toolOptions, mcp.WithDescription(a.Description))
headers := make(map[string]interface{})
queries := make(map[string]interface{})
path := make(map[string]interface{})
for _, v := range a.Params {
p := map[string]interface{}{
"type": "string",
"required": v.Required,
"description": v.Description,
}
switch v.In {
case "header":
headers[v.Name] = p
case "query":
queries[v.Name] = p
case "path":
path[v.Name] = p
}
}
if len(headers) > 0 {
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPHeader, mcp.Properties(headers), mcp.Description("request headers.")))
}
if len(queries) > 0 {
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPQuery, mcp.Properties(queries), mcp.Description("request queries.")))
}
if len(path) > 0 {
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPPath, mcp.Properties(path), mcp.Description("request path params.")))
}
if a.Body != nil {
type Schema struct {
Type string `mapstructure:"type"`
Properties map[string]interface{} `mapstructure:"properties"`
Items interface{} `mapstructure:"items"`
}
var tmp Schema
err = mapstructure.Decode(a.Body, &tmp)
if err != nil {
return err
}
//switch a.ContentType {
//case "application/json":
switch tmp.Type {
case "object":
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPBody, mcp.Properties(tmp.Properties), mcp.Description("request body,it is avalible when method is POST、PUT、PATCH.")))
case "array":
toolOptions = append(toolOptions, mcp.WithArray(mcp_server.MCPBody, mcp.Items(tmp.Items), mcp.Description("request body,it is avalible when method is POST、PUT、PATCH.")))
}
//case "application/x-www-form-urlencoded":
// toolOptions = append(toolOptions, mcp.WithString(mcp_server.MCPBody, mcp.Items(tmp.Items), mcp.Description("request body,it is avalible when method is POST、PUT、PATCH.")))
}
tools = append(tools, mcp_server.NewTool(a.Summary, a.Path, a.Method, a.ContentType, toolOptions...))
}
mcp_server.SetSSEServer(sid, name, version, tools...)
return nil
return mcp_server.SetServerByOpenapi(sid, name, version, commitDoc.Data.Content)
}
func (i *imlPublishModule) Detail(ctx context.Context, serviceId string, id string) (*dto.PublishDetail, error) {
+2 -62
View File
@@ -14,12 +14,8 @@ import (
"github.com/APIParkLab/APIPark/common"
"github.com/mitchellh/mapstructure"
"github.com/eolinker/go-common/register"
"github.com/mark3labs/mcp-go/mcp"
mcp_server "github.com/APIParkLab/APIPark/mcp-server"
"github.com/APIParkLab/APIPark/service/release"
@@ -395,67 +391,11 @@ func (i *imlServiceModule) updateMCPServer(ctx context.Context, sid string, name
if err != nil {
return fmt.Errorf("get api doc commit error: %w", err)
}
mcpInfo, err := mcp_server.ConvertMCPFromOpenAPI3Data([]byte(commitDoc.Data.Content))
if err != nil {
return fmt.Errorf("convert mcp from openapi3 data error: %w", err)
}
tools := make([]mcp_server.ITool, 0, len(mcpInfo.Apis))
for _, a := range mcpInfo.Apis {
toolOptions := make([]mcp.ToolOption, 0, len(a.Params)+2)
toolOptions = append(toolOptions, mcp.WithDescription(a.Description))
headers := make(map[string]interface{})
queries := make(map[string]interface{})
path := make(map[string]interface{})
for _, v := range a.Params {
p := map[string]interface{}{
"type": "string",
"required": v.Required,
"description": v.Description,
}
switch v.In {
case "header":
headers[v.Name] = p
case "query":
queries[v.Name] = p
case "path":
path[v.Name] = p
}
}
if len(headers) > 0 {
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPHeader, mcp.Properties(headers), mcp.Description("request headers.")))
}
if len(queries) > 0 {
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPQuery, mcp.Properties(queries), mcp.Description("request queries.")))
}
if len(path) > 0 {
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPPath, mcp.Properties(path), mcp.Description("request path params.")))
}
if a.Body != nil {
type Schema struct {
Type string `mapstructure:"type"`
Properties map[string]interface{} `mapstructure:"properties"`
Items interface{} `mapstructure:"items"`
}
var tmp Schema
err = mapstructure.Decode(a.Body, &tmp)
if err != nil {
return err
}
switch tmp.Type {
case "object":
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPBody, mcp.Properties(tmp.Properties), mcp.Description("request body,it is avalible when method is POST、PUT、PATCH.")))
case "array":
toolOptions = append(toolOptions, mcp.WithArray(mcp_server.MCPBody, mcp.Items(tmp.Items), mcp.Description("request body,it is avalible when method is POST、PUT、PATCH.")))
}
}
tools = append(tools, mcp_server.NewTool(a.Summary, a.Path, a.Method, a.ContentType, toolOptions...))
}
mcp_server.SetSSEServer(sid, name, version, tools...)
return nil
return mcp_server.SetServerByOpenapi(sid, name, version, commitDoc.Data.Content)
}
func (i *imlServiceModule) deleteMCPServer(ctx context.Context, sid string) {
mcp_server.DelSSEServer(sid)
mcp_server.DelServer(sid)
}
func (i *imlServiceModule) ExportAll(ctx context.Context) ([]*service_dto.ExportService, error) {
+9 -1
View File
@@ -15,15 +15,23 @@ func (p *plugin) mcpAPIs() []pm3.Api {
globalMessagePath := fmt.Sprintf("/openapi/v1/%s/message", strings.Trim(mcp_server.GlobalBasePath, "/"))
serviceMessagePath := fmt.Sprintf("/openapi/v1/%s/:serviceId/message", strings.Trim(mcp_server.ServiceBasePath, "/"))
serviceSSEPath := fmt.Sprintf("/openapi/v1/%s/:serviceId/sse", strings.Trim(mcp_server.ServiceBasePath, "/"))
serviceStreamablePath := fmt.Sprintf("/openapi/v1/%s/:serviceId/mcp", strings.Trim(mcp_server.ServiceBasePath, "/"))
ignore.IgnorePath("openapi", http.MethodPost, globalMessagePath)
ignore.IgnorePath("openapi", http.MethodGet, serviceSSEPath)
ignore.IgnorePath("openapi", http.MethodPost, serviceMessagePath)
ignore.IgnorePath("openapi", http.MethodGet, serviceStreamablePath)
ignore.IgnorePath("openapi", http.MethodPost, serviceStreamablePath)
ignore.IgnorePath("openapi", http.MethodDelete, serviceStreamablePath)
return []pm3.Api{
pm3.CreateApiSimple(http.MethodGet, fmt.Sprintf("/openapi/v1/%s/sse", strings.Trim(mcp_server.GlobalBasePath, "/")), p.mcpController.GlobalHandleSSE),
pm3.CreateApiSimple(http.MethodPost, globalMessagePath, p.mcpController.GlobalHandleMessage),
pm3.CreateApiSimple(http.MethodGet, serviceSSEPath, p.mcpController.ServiceHandleSSE),
pm3.CreateApiSimple(http.MethodPost, serviceMessagePath, p.mcpController.ServiceHandleMessage),
pm3.CreateApiSimple(http.MethodPost, serviceStreamablePath, p.mcpController.ServiceHandleStreamHTTP),
pm3.CreateApiSimple(http.MethodDelete, serviceStreamablePath, p.mcpController.ServiceHandleStreamHTTP),
pm3.CreateApiSimple(http.MethodGet, serviceStreamablePath, p.mcpController.ServiceHandleStreamHTTP),
}
}
+1 -1
View File
@@ -59,7 +59,7 @@ type Doc struct {
Content string `gorm:"type:longtext;null;column:content;comment:文档内容"`
Updater string `gorm:"size:36;not null;column:updater;comment:更新人;index:updater" aovalue:"updater"`
UpdateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:update_at;comment:更新时间"`
APICount int64 `gorm:"type:int(11);not null;column:api_count;comment:接口数量"`
APICount int64 `gorm:"type:int(11);not null;column:api_count;default:0;comment:接口数量"`
}
func (i *Doc) TableName() string {