mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-06-04 10:13:53 +08:00
Merge pull request #342 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
This commit is contained in:
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",
|
||||
|
||||
@@ -972,5 +972,17 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -994,5 +994,17 @@
|
||||
"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 開発者コンソールのアプリ認証情報と基本情報ページにあります"
|
||||
}
|
||||
|
||||
@@ -923,5 +923,17 @@
|
||||
"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 参数位于飞书开发人员控制台中的应用程序凭证和基础信息页面上"
|
||||
}
|
||||
|
||||
@@ -994,5 +994,17 @@
|
||||
"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 參數位於飛書開發人員控制台中的應用程式憑證與基礎資訊頁面"
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
form: 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,form?:string})
|
||||
selectedMemberGroupId?:string
|
||||
}
|
||||
|
||||
|
||||
@@ -1,221 +1,372 @@
|
||||
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 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)
|
||||
}
|
||||
})
|
||||
},[])
|
||||
|
||||
|
||||
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: 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)
|
||||
/**
|
||||
* 飞书登录
|
||||
* @param feishuCode 飞书 code
|
||||
*/
|
||||
const feishuLogin = async (feishuCode: string) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const { code, msg } = await fetchData<BasicResponse<null>>('account/login/feishu', {
|
||||
method: 'POST',
|
||||
eoBody: {
|
||||
code: feishuCode
|
||||
}
|
||||
}
|
||||
const login = async () => {
|
||||
if (formRef.current) {
|
||||
const values = await formRef.current.validateFields();
|
||||
fetchLogin(values);
|
||||
})
|
||||
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
dispatch({ type: 'LOGIN' })
|
||||
const callbackUrl = new URLSearchParams(window.location.search).get('callbackUrl')
|
||||
if (callbackUrl && callbackUrl !== 'null') {
|
||||
navigate(callbackUrl)
|
||||
} else {
|
||||
navigate(state.mainPage)
|
||||
}
|
||||
};
|
||||
|
||||
const loginAsGuest = ()=>{
|
||||
fetchLogin({username:'guest',password:'12345678'})
|
||||
} else {
|
||||
dispatch({ type: 'LOGOUT' })
|
||||
message.error(msg)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
check()
|
||||
getSystemInfo()
|
||||
}, []);
|
||||
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.app_id)
|
||||
setAllowFeishuLogin(true)
|
||||
}
|
||||
const code = query.get('code')
|
||||
if (code) {
|
||||
feishuLogin(code)
|
||||
}
|
||||
setSpinning(false)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
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 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 })
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
<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>
|
||||
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 openFeishuLogin = () => {
|
||||
const href = location.href
|
||||
const authUrl = `https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id=${feishuAppId}&redirect_uri=${href}`
|
||||
window.location.href = authUrl
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
check()
|
||||
getSystemInfo()
|
||||
}, [])
|
||||
|
||||
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>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
{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}
|
||||
>
|
||||
<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,167 @@
|
||||
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 })))
|
||||
} 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.info?.config?.clientId || '',
|
||||
clientSecret: data.info?.config?.clientSecret || '',
|
||||
enabled: data.info?.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')
|
||||
@@ -184,6 +191,34 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消费者 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 || $t(RESPONSE_TIPS.error))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局 MCP 跳转
|
||||
*/
|
||||
@@ -229,12 +264,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
|
||||
@@ -345,6 +380,10 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
|
||||
getGlobalMcpConfig()
|
||||
setMcpServerUrl('mcp/global/sse')
|
||||
getGlobalKeysList()
|
||||
} else if (type === 'consumer'){
|
||||
getConsumerMcpConfig()
|
||||
setMcpServerUrl(`mcp/service/${consumerParams?.consumerId}/sse`)
|
||||
getServiceKeysList(consumerParams?.consumerId)
|
||||
} else {
|
||||
service?.basic.enableMcp && setMcpServerUrl(`mcp/service/${serviceId}/sse`)
|
||||
getServiceKeysList()
|
||||
@@ -362,6 +401,7 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
|
||||
useEffect(() => {
|
||||
initTabsData()
|
||||
type === 'global' && getGlobalMcpConfig()
|
||||
type === 'consumer' && getConsumerMcpConfig()
|
||||
}, [state.language])
|
||||
/**
|
||||
* 切换标签
|
||||
@@ -408,7 +448,7 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
|
||||
}
|
||||
connectMcpServer()
|
||||
}
|
||||
}, [mcpServerUrl, ...(type === 'global' ? [state.language] : [])])
|
||||
}, [mcpServerUrl, ...(type === 'global' || type === 'consumer' ? [state.language] : [])])
|
||||
/**
|
||||
* 获取 MCP tools
|
||||
*/
|
||||
@@ -533,7 +573,7 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
|
||||
<div className="tab-content font-semibold my-[10px]">API Key</div>
|
||||
{apiKeyList.length ? (
|
||||
<>
|
||||
{type === 'global' ? (
|
||||
{type === 'global' || type === 'consumer' ? (
|
||||
<>
|
||||
<Select
|
||||
showSearch
|
||||
|
||||
@@ -14,6 +14,7 @@ export const MemberDropdownModal = forwardRef<MemberDropdownModalHandle,MemberDr
|
||||
const {fetchData} = useFetch()
|
||||
const [departmentList, setDepartmentList] = useState<DepartmentListItem[]>([])
|
||||
const { state } = useGlobalContext()
|
||||
const [disableEditMemberData] = useState<boolean>(entity?.form !== 'self-build')
|
||||
|
||||
const save:()=>Promise<boolean | string> = ()=>{
|
||||
let url:string
|
||||
@@ -182,27 +183,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
|
||||
|
||||
@@ -396,7 +396,7 @@ const MemberList = () => {
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
okButtonProps: {
|
||||
disabled: isActionAllowed(type)
|
||||
disabled: isActionAllowed(type) || entity?.form !== 'self-build'
|
||||
},
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
|
||||
@@ -32,6 +32,7 @@ export default function ManagementInsidePage() {
|
||||
const TENANT_MANAGEMENT_APP_MENU: MenuProps['items'] = useMemo(
|
||||
() => [
|
||||
getItem($t('订阅的服务'), 'service', undefined, undefined, undefined, 'team.application.subscription.view'),
|
||||
getItem($t('MCP 服务'), 'mcp', undefined, undefined, undefined, 'team.consumer.mcp.view'),
|
||||
getItem($t('访问授权'), 'authorization', undefined, undefined, undefined, 'team.consumer.authorization.view'),
|
||||
getItem($t('消费者管理'), 'setting', undefined, undefined, undefined, 'team.application.application.view')
|
||||
],
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user