From 393271d72a193eb87cca9248cfb16fe91346fe1b Mon Sep 17 00:00:00 2001 From: lcx Date: Thu, 17 Jul 2025 11:29:20 +0800 Subject: [PATCH] feat: Add Feishu OAuth login & consumer-grade MCP --- .../packages/common/src/assets/feishu.png | Bin 0 -> 6658 bytes .../src/components/aoplatform/BasicLayout.tsx | 2 +- .../packages/common/src/const/permissions.ts | 14 + .../src/contexts/GlobalStateContext.tsx | 9 +- .../packages/common/src/hooks/pluginLoader.ts | 10 + .../common/src/locales/keyHashMap.json | 12 + .../common/src/locales/scan/en-US.json | 14 +- .../common/src/locales/scan/ja-JP.json | 14 +- .../common/src/locales/scan/zh-CN.json | 14 +- .../common/src/locales/scan/zh-TW.json | 14 +- frontend/packages/core/src/const/const.tsx | 18 + .../packages/core/src/const/member/type.ts | 3 +- frontend/packages/core/src/pages/Login.tsx | 569 +++++++++++------- .../packages/core/src/pages/auth/Auth.tsx | 167 +++++ .../mcpService/IntegrationAIContainer.tsx | 56 +- .../src/pages/member/MemberDropdownModal.tsx | 8 +- .../core/src/pages/member/MemberList.tsx | 2 +- .../management/ManagementInsidePage.tsx | 1 + .../serviceHub/management/mcpContent.tsx | 40 ++ 19 files changed, 739 insertions(+), 228 deletions(-) create mode 100644 frontend/packages/common/src/assets/feishu.png create mode 100644 frontend/packages/core/src/pages/auth/Auth.tsx create mode 100644 frontend/packages/market/src/pages/serviceHub/management/mcpContent.tsx diff --git a/frontend/packages/common/src/assets/feishu.png b/frontend/packages/common/src/assets/feishu.png new file mode 100644 index 0000000000000000000000000000000000000000..1476ee5b859266ea67105b3c3c15c75e233c93ca GIT binary patch literal 6658 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 G*na>O(#g;O literal 0 HcmV?d00001 diff --git a/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx b/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx index 5bf074d5..4ee40750 100644 --- a/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx +++ b/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx @@ -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: ( + + {allowFeishuLogin && ( + <> + + + + + + + )} + {allowGuest && ( + <> + + + + + + + )} + - {/*
*/} -
-
- - - -
- -
-
-
-
- - - - - - - - - - - - { - allowGuest && <> - - - - - - - } - -
-
-
- -
-

- {$t('Version (0)-(1)',[state?.version,state?.updateDate])}, {$t(state?.powered || '-')} -

- -
-
- - ); + + + +
+

+ {$t('Version (0)-(1)', [state?.version, state?.updateDate])}, {$t(state?.powered || '-')} +

+ +
+ + + ) } -export default Login; \ No newline at end of file +export default Login diff --git a/frontend/packages/core/src/pages/auth/Auth.tsx b/frontend/packages/core/src/pages/auth/Auth.tsx new file mode 100644 index 00000000..cd337ea4 --- /dev/null +++ b/frontend/packages/core/src/pages/auth/Auth.tsx @@ -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(null) + const { state } = useGlobalContext() + const [thirdPartyDrivers, setThirdPartyDrivers] = useState<{ label: string; value: string }[]>([]) + useEffect(() => { + forceUpdate({}) + }, [state.language]) + const onFinish = () => { + form.validateFields().then((value) => { + return fetchData>(`/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>(`/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 ( + + +
+ + label={$t('授权类型')} + name="authType" + rules={[{ required: true, message: $t('请选择授权类型') }]} + > + + + + + label={$t('APP Secret')} + name="clientSecret" + rules={[{ required: true, whitespace: true, message: $t('请输入APP Secret') }]} + extra={$t('APP Secret 参数位于飞书开发人员控制台中的应用程序凭证和基础信息页面上')} + > + + + + label={$t('启用授权')} name="enabled" valuePropName="checked"> + + + + + + + + + +
+
+ ) +} + +export default Auth diff --git a/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx b/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx index e566305f..48486726 100644 --- a/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx +++ b/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx @@ -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( ({ @@ -69,8 +75,9 @@ export const IntegrationAIContainer = forwardRef { /** 当前激活的标签 */ const [activeTab, setActiveTab] = useState(type === 'service' ? 'openApi' : 'mcp') @@ -184,6 +191,34 @@ export const IntegrationAIContainer = forwardRef { + fetchData>('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 { + const getServiceKeysList = (consumerId?: string) => { fetchData>(`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 { initTabsData() type === 'global' && getGlobalMcpConfig() + type === 'consumer' && getConsumerMcpConfig() }, [state.language]) /** * 切换标签 @@ -408,7 +448,7 @@ export const IntegrationAIContainer = forwardRefAPI Key {apiKeyList.length ? ( <> - {type === 'global' ? ( + {type === 'global' || type === 'consumer' ? ( <> + label={$t("邮箱")} name="email" rules={[{required: true,whitespace:true },{type:"email",message: $t(VALIDATE_MESSAGE.email)}]} > - + label={$t("密码")} name="password" rules={[{required: type === 'addMember',whitespace:true }]} > - + label={$t("部门")} name="departmentIds" > { width: 600, okText: $t('确认'), okButtonProps: { - disabled: isActionAllowed(type) + disabled: isActionAllowed(type) || entity?.form !== 'self-build' }, cancelText: $t('取消'), closable: true, diff --git a/frontend/packages/market/src/pages/serviceHub/management/ManagementInsidePage.tsx b/frontend/packages/market/src/pages/serviceHub/management/ManagementInsidePage.tsx index 96d16e22..708e63dd 100644 --- a/frontend/packages/market/src/pages/serviceHub/management/ManagementInsidePage.tsx +++ b/frontend/packages/market/src/pages/serviceHub/management/ManagementInsidePage.tsx @@ -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') ], diff --git a/frontend/packages/market/src/pages/serviceHub/management/mcpContent.tsx b/frontend/packages/market/src/pages/serviceHub/management/mcpContent.tsx new file mode 100644 index 00000000..a0f414ea --- /dev/null +++ b/frontend/packages/market/src/pages/serviceHub/management/mcpContent.tsx @@ -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([]) + const [, forceUpdate] = useState(null) + const { teamId, appId } = useParams() + const { state } = useGlobalContext() + const handleToolsChange = (value: Tool[]) => { + setTools(value) + } + useEffect(() => { + forceUpdate({}) + }, [state.language]) + return ( +
+
+ {$t('MCP 服务')} +
+
+
+ + +
+
+
+ ) +} + +export default mcpContent