diff --git a/ui/scripts/downLoadIcon.cjs b/ui/scripts/downLoadIcon.cjs index b9a242c4..2778ced5 100644 --- a/ui/scripts/downLoadIcon.cjs +++ b/ui/scripts/downLoadIcon.cjs @@ -1,64 +1,23 @@ -const fs = require('fs') -const path = require('path') - -const Axios = require('axios') +const fs = require("fs"); +const path = require("path"); async function downloadFile(url) { - try { - const iconPath = path.resolve(__dirname, '../src/assets/fonts/iconfont.js') - const iconDir = path.dirname(iconPath) - - // 检查目录是否存在,如果不存在则创建 - if (!fs.existsSync(iconDir)) { - console.log(`目录 ${iconDir} 不存在,正在创建...`) - fs.mkdirSync(iconDir, { recursive: true }) - console.log('目录创建成功') - } - - console.log(`开始下载图标文件到: ${iconPath}`) - - const writer = fs.createWriteStream(iconPath) - const response = await Axios({ - url: `https:${url}`, - method: 'GET', - responseType: 'stream', - timeout: 30000, // 30秒超时 - }) - - response.data.pipe(writer) - - return new Promise((resolve, reject) => { - writer.on('finish', () => { - console.log('图标文件下载成功!') - resolve() - }) - writer.on('error', (err) => { - console.error('写入文件时出错:', err.message) - reject(err) - }) - }) - } catch (error) { - console.error('下载过程中出错:', error.message) - throw error - } -} + const iconPath = path.resolve(__dirname, "../src/assets/fonts/iconfont.js"); + const iconDir = path.dirname(iconPath); -async function main() { - const argument = process.argv.splice(2) - - if (!argument[0]) { - console.error('错误: 请提供下载URL作为参数') - console.log('使用方法: node downLoadIcon.cjs ') - process.exit(1) - } - - try { - await downloadFile(argument[0]) - console.log('所有操作完成!') - } catch (error) { - console.error('脚本执行失败:', error.message) - process.exit(1) + // 检查目录是否存在,不存在则创建 + if (!fs.existsSync(iconDir)) { + fs.mkdirSync(iconDir, { recursive: true }); + console.log(`目录 ${iconDir} 已创建`); } + + const response = await fetch(`https:${url}`, { + method: "GET", + // responseType: "stream", // fetch 不支持此参数 + }).then((res) => res.text()); + fs.writeFileSync(iconPath, response); + console.log("Download Icon Success"); } +let argument = process.argv.splice(2); +downloadFile(argument[0]); -main() diff --git a/ui/src/api/User.ts b/ui/src/api/User.ts index 35548cb9..8646910c 100644 --- a/ui/src/api/User.ts +++ b/ui/src/api/User.ts @@ -23,6 +23,7 @@ import { DomainListUserResp, DomainLoginReq, DomainLoginResp, + DomainOAuthURLResp, DomainRegisterReq, DomainSetting, DomainUpdateSettingReq, @@ -32,6 +33,8 @@ import { GetListAdminUserParams, GetListUserParams, GetLoginHistoryParams, + GetUserOauthCallbackParams, + GetUserOauthSignupOrInParams, WebResp, } from "./types"; @@ -401,6 +404,66 @@ export const getLoginHistory = ( ...params, }); +/** + * @description 用户 OAuth 回调 + * + * @tags User + * @name GetUserOauthCallback + * @summary 用户 OAuth 回调 + * @request GET:/api/v1/user/oauth/callback + * @response `200` `(WebResp & { + data?: string, + +})` OK + */ + +export const getUserOauthCallback = ( + query: GetUserOauthCallbackParams, + params: RequestParams = {}, +) => + request< + WebResp & { + data?: string; + } + >({ + path: `/api/v1/user/oauth/callback`, + method: "GET", + query: query, + type: ContentType.Json, + format: "json", + ...params, + }); + +/** + * @description 用户 OAuth 登录或注册 + * + * @tags User + * @name GetUserOauthSignupOrIn + * @summary 用户 OAuth 登录或注册 + * @request GET:/api/v1/user/oauth/signup-or-in + * @response `200` `(WebResp & { + data?: DomainOAuthURLResp, + +})` OK + */ + +export const getUserOauthSignupOrIn = ( + query: GetUserOauthSignupOrInParams, + params: RequestParams = {}, +) => + request< + WebResp & { + data?: DomainOAuthURLResp; + } + >({ + path: `/api/v1/user/oauth/signup-or-in`, + method: "GET", + query: query, + type: ContentType.Json, + format: "json", + ...params, + }); + /** * @description 注册用户 * diff --git a/ui/src/api/types.ts b/ui/src/api/types.ts index 9fe3b17b..e85cc370 100644 --- a/ui/src/api/types.ts +++ b/ui/src/api/types.ts @@ -16,6 +16,11 @@ export enum ConstsUserStatus { UserStatusLocked = "locked", } +export enum ConstsUserPlatform { + UserPlatformEmail = "email", + UserPlatformDingTalk = "dingtalk", +} + export enum ConstsModelType { ModelTypeLLM = "llm", ModelTypeCoder = "coder", @@ -312,6 +317,10 @@ export interface DomainModelTokenUsageResp { total_output?: number; } +export interface DomainOAuthURLResp { + url?: string; +} + export interface DomainProviderModel { /** 模型列表 */ models?: DomainModelBasic[]; @@ -333,6 +342,8 @@ export interface DomainSetting { created_at?: number; /** 是否禁用密码登录 */ disable_password_login?: boolean; + /** 是否开启钉钉OAuth */ + enable_dingtalk_oauth?: boolean; /** 是否开启SSO */ enable_sso?: boolean; /** 是否强制两步验证 */ @@ -419,8 +430,14 @@ export interface DomainUpdateModelReq { } export interface DomainUpdateSettingReq { + /** 钉钉客户端ID */ + dingtalk_client_id?: string; + /** 钉钉客户端密钥 */ + dingtalk_client_secret?: string; /** 是否禁用密码登录 */ disable_password_login?: boolean; + /** 是否开启钉钉OAuth */ + enable_dingtalk_oauth?: boolean; /** 是否开启SSO */ enable_sso?: boolean; /** 是否强制两步验证 */ @@ -637,3 +654,17 @@ export interface GetLoginHistoryParams { /** 每页多少条记录 */ size?: number; } + +export interface GetUserOauthCallbackParams { + code: string; + state: string; +} + +export interface GetUserOauthSignupOrInParams { + /** 第三方平台 dingtalk */ + platform: "email" | "dingtalk"; + /** 登录成功后跳转的 URL */ + redirect_url?: string; + /** 会话ID */ + session_id?: string; +} diff --git a/ui/src/assets/fonts/iconfont.js b/ui/src/assets/fonts/iconfont.js index 19f3f8da..acbe1614 100644 --- a/ui/src/assets/fonts/iconfont.js +++ b/ui/src/assets/fonts/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_4940939='',(c=>{var a=(l=(l=document.getElementsByTagName("script"))[l.length-1]).getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var t,o,i,h,e,n=function(a,l){l.parentNode.insertBefore(a,l)};if(a&&!c.__iconfont__svg__cssinject__){c.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}t=function(){var a,l=document.createElement("div");l.innerHTML=c._iconfont_svg_string_4940939,(l=l.getElementsByTagName("svg")[0])&&(l.setAttribute("aria-hidden","true"),l.style.position="absolute",l.style.width=0,l.style.height=0,l.style.overflow="hidden",l=l,(a=document.body).firstChild?n(l,a.firstChild):a.appendChild(l))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(t,0):(o=function(){document.removeEventListener("DOMContentLoaded",o,!1),t()},document.addEventListener("DOMContentLoaded",o,!1)):document.attachEvent&&(i=t,h=c.document,e=!1,d(),h.onreadystatechange=function(){"complete"==h.readyState&&(h.onreadystatechange=null,m())})}function m(){e||(e=!0,i())}function d(){try{h.documentElement.doScroll("left")}catch(a){return void setTimeout(d,50)}m()}})(window); \ No newline at end of file +window._iconfont_svg_string_4940939='',(c=>{var a=(l=(l=document.getElementsByTagName("script"))[l.length-1]).getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var t,o,i,h,e,n=function(a,l){l.parentNode.insertBefore(a,l)};if(a&&!c.__iconfont__svg__cssinject__){c.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}t=function(){var a,l=document.createElement("div");l.innerHTML=c._iconfont_svg_string_4940939,(l=l.getElementsByTagName("svg")[0])&&(l.setAttribute("aria-hidden","true"),l.style.position="absolute",l.style.width=0,l.style.height=0,l.style.overflow="hidden",l=l,(a=document.body).firstChild?n(l,a.firstChild):a.appendChild(l))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(t,0):(o=function(){document.removeEventListener("DOMContentLoaded",o,!1),t()},document.addEventListener("DOMContentLoaded",o,!1)):document.attachEvent&&(i=t,h=c.document,e=!1,d(),h.onreadystatechange=function(){"complete"==h.readyState&&(h.onreadystatechange=null,m())})}function m(){e||(e=!0,i())}function d(){try{h.documentElement.doScroll("left")}catch(a){return void setTimeout(d,50)}m()}})(window); \ No newline at end of file diff --git a/ui/src/components/form/index.tsx b/ui/src/components/form/index.tsx index 3c52622d..d5d37b48 100644 --- a/ui/src/components/form/index.tsx +++ b/ui/src/components/form/index.tsx @@ -1,11 +1,10 @@ -'use client'; import { styled, FormLabel } from '@mui/material'; export const StyledFormLabel = styled(FormLabel)(({ theme }) => ({ display: 'block', color: theme.vars.palette.text.primary, - fontSize: 16, - fontWeight: 500, + fontSize: 14, + fontWeight: 400, marginBottom: theme.spacing(1), [theme.breakpoints.down('sm')]: { fontSize: 14, diff --git a/ui/src/components/markDown/index.tsx b/ui/src/components/markDown/index.tsx index fe0fe435..3a969649 100644 --- a/ui/src/components/markDown/index.tsx +++ b/ui/src/components/markDown/index.tsx @@ -97,6 +97,10 @@ const MarkDown = ({ const answer = processContent(content); + console.log(answer); + + console.log(content); + if (content.length === 0) return null; return ( @@ -446,12 +450,13 @@ const MarkDown = ({ ...rest }: React.HTMLAttributes) { const match = /language-(\w+)/.exec(className || ''); + console.log(children, rest); return match ? ( { if (navigator.clipboard) { navigator.clipboard.writeText( diff --git a/ui/src/pages/auth/index.tsx b/ui/src/pages/auth/index.tsx index cbb7061a..d41daeb9 100644 --- a/ui/src/pages/auth/index.tsx +++ b/ui/src/pages/auth/index.tsx @@ -12,17 +12,20 @@ import { Grid2 as Grid, InputAdornment, IconButton, + Divider, + Stack, } from '@mui/material'; -import { Icon } from '@c-x/ui'; +import { Icon, message } from '@c-x/ui'; // @ts-ignore import { AestheticFluidBg } from '@/assets/jsm/AestheticFluidBg.module.js'; import { useSearchParams } from 'react-router-dom'; -import { postLogin } from '@/api/User'; +import { postLogin, getUserOauthSignupOrIn, getGetSetting } from '@/api/User'; import { useForm, Controller } from 'react-hook-form'; import { styled } from '@mui/material/styles'; +import { useRequest } from 'ahooks'; // 样式化组件 const StyledContainer = styled(Container)(({ theme }) => ({ @@ -111,6 +114,7 @@ const AuthPage = () => { const [showPassword, setShowPassword] = useState(false); const [searchParams] = useSearchParams(); + const { data: loginSetting = {} } = useRequest(getGetSetting); const { control, @@ -132,7 +136,8 @@ const AuthPage = () => { try { const sessionId = searchParams.get('session_id'); if (!sessionId) { - throw new Error('缺少会话ID参数'); + message.error('缺少会话ID参数'); + return; } // 用户登录 @@ -245,17 +250,6 @@ const AuthPage = () => { /> ); - // 渲染错误提示 - const renderErrorAlert = () => { - if (!error) return null; - - return ( - - {error} - - ); - }; - // 渲染登录按钮 const renderLoginButton = () => ( @@ -271,6 +265,32 @@ const AuthPage = () => { ); + const onDingdingLogin = () => { + getUserOauthSignupOrIn({ + platform: 'dingtalk', + redirect_url: window.location.origin + window.location.pathname, + // @ts-ignore + session_id: searchParams.get('session_id') || null, + }).then((res) => { + if (res.url) { + window.location.href = res.url; + } + }); + }; + + const dingdingLogin = () => { + return ( + + + 使用其他方式登录 + + + + + + ); + }; + // 渲染登录表单 const renderLoginForm = () => ( <> @@ -284,19 +304,27 @@ const AuthPage = () => { {renderUsernameField()} - {renderPasswordField()} - {renderErrorAlert()} {renderLoginButton()} ); + useEffect(() => { + const redirect_url = searchParams.get('redirect_url'); + if (redirect_url) { + window.location.href = redirect_url; + } + }, []); + return ( - {renderLoginForm()} + + {!loginSetting.disable_password_login && renderLoginForm()} + {loginSetting.enable_dingtalk_oauth && dingdingLogin()} + ); }; diff --git a/ui/src/pages/chat/chatDetailModal.tsx b/ui/src/pages/chat/chatDetailModal.tsx index d3fd4405..8712a1bb 100644 --- a/ui/src/pages/chat/chatDetailModal.tsx +++ b/ui/src/pages/chat/chatDetailModal.tsx @@ -78,7 +78,7 @@ const ChatDetailModal = ({ width: 700, }} > - {data?.question?.replace(/^|<\/task>$/g, '') || '-'} + 对话记录-{data?.user?.username} } width={800} diff --git a/ui/src/pages/chat/index.tsx b/ui/src/pages/chat/index.tsx index b3fdd8af..63b4594c 100644 --- a/ui/src/pages/chat/index.tsx +++ b/ui/src/pages/chat/index.tsx @@ -56,7 +56,7 @@ const Chat = () => { dataIndex: 'question', title: '任务', render(value: string, record) { - const cleanValue = value?.replace(/^|<\/task>$/g, '') || value; + const cleanValue = value?.replace(/<\/?task>/g, '') || value; return ( setChatDetailModal(record)} diff --git a/ui/src/pages/completion/completionDetailModal.tsx b/ui/src/pages/completion/completionDetailModal.tsx index 32b89451..681e1309 100644 --- a/ui/src/pages/completion/completionDetailModal.tsx +++ b/ui/src/pages/completion/completionDetailModal.tsx @@ -33,9 +33,9 @@ const ChatDetailModal = ({ if (!data) return; getChatInfo({ id: data.id! }).then((res) => { setContent( - `${ - res.content || '' - }` + data.program_language + ? `\`\`\`${data.program_language}\n${res.content || ''}\n\`\`\`` + : res.content || '' ); }); // getConversationChatDetailModal({ id }).then((res) => { diff --git a/ui/src/pages/invite/index.tsx b/ui/src/pages/invite/index.tsx index 354859ba..777ba616 100644 --- a/ui/src/pages/invite/index.tsx +++ b/ui/src/pages/invite/index.tsx @@ -19,9 +19,14 @@ import { InputAdornment, IconButton, CircularProgress, + Stack, } from '@mui/material'; import { useRequest } from 'ahooks'; -import { postRegister } from '@/api/User'; +import { + postRegister, + getUserOauthSignupOrIn, + getGetSetting, +} from '@/api/User'; import { Icon } from '@c-x/ui'; import DownloadIcon from '@mui/icons-material/Download'; @@ -63,9 +68,9 @@ const StepCard = styled(Box)(({ theme }) => ({ })); const Invite = () => { - const { id } = useParams(); + const { id, step } = useParams(); const [showPassword, setShowPassword] = useState(false); - + const { data: loginSetting = {} } = useRequest(getGetSetting); const { control, handleSubmit, @@ -80,7 +85,7 @@ const Invite = () => { const { runAsync: register, loading } = useRequest(postRegister, { manual: true, }); - const [activeStep, setActiveStep] = useState(0); + const [activeStep, setActiveStep] = useState(step ? parseInt(step) : 1); const onNext = () => { setActiveStep(activeStep + 1); @@ -107,10 +112,19 @@ const Invite = () => { }); }, []); + const onDingdingLogin = () => { + getUserOauthSignupOrIn({ + platform: 'dingtalk', + redirect_url: `${window.location.origin}/invite/${id}/2`, + }).then((res) => { + window.location.href = res.url!; + }); + }; + const renderStepContent = () => { switch (activeStep) { - case 0: - return ( + case 1: + return !loginSetting.enable_dingtalk_oauth ? ( @@ -229,9 +243,21 @@ const Invite = () => { + ) : ( + + + ); - case 1: + case 2: return ( @@ -262,7 +288,7 @@ const Invite = () => { ); - case 2: + case 3: return ( diff --git a/ui/src/pages/user/dingdingLoginSettingModal.tsx b/ui/src/pages/user/dingdingLoginSettingModal.tsx new file mode 100644 index 00000000..2d571f24 --- /dev/null +++ b/ui/src/pages/user/dingdingLoginSettingModal.tsx @@ -0,0 +1,131 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, message, Loading } from '@c-x/ui'; +import { useForm, Controller } from 'react-hook-form'; +import { StyledFormLabel } from '@/components/form'; +import { putUpdateSetting } from '@/api/User'; +import { + Box, + Typography, + IconButton, + Paper, + TextField, + Stack, +} from '@mui/material'; + +const DingingLoginSettingModal = ({ + open, + onClose, + onOk, +}: { + open: boolean; + onClose: () => void; + onOk: () => void; +}) => { + const { + control, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + defaultValues: { + dingtalk_client_id: '', + dingtalk_client_secret: '', + // title: '', + }, + }); + + useEffect(() => { + if (open) { + reset(); + } + }, [open]); + + const onSubmit = handleSubmit((data) => { + putUpdateSetting({ ...data, enable_dingtalk_oauth: true }).then(() => { + message.success('设置成功'); + onClose(); + onOk(); + }); + }); + + return ( + + + + Client ID + ( + + )} + /> + + + Client Secret + ( + + )} + /> + + {/* + 标题名称,默认为 身份认证-钉钉登录 + ( + { + field.onChange(e.target.value); + }} + /> + )} + /> + */} + + + ); +}; + +export default DingingLoginSettingModal; diff --git a/ui/src/pages/user/index.tsx b/ui/src/pages/user/index.tsx index 62a80856..271351c1 100644 --- a/ui/src/pages/user/index.tsx +++ b/ui/src/pages/user/index.tsx @@ -1,16 +1,30 @@ -import React from 'react'; +import React, { useState } from 'react'; import Card from '@/components/card'; -import { Grid2 as Grid, Stack, styled, Switch } from '@mui/material'; +import { + Grid2 as Grid, + Stack, + styled, + Switch, + Button, + Box, + Select, + MenuItem, + Radio, +} from '@mui/material'; +import { Icon, Modal } from '@c-x/ui'; import { useRequest } from 'ahooks'; import { getGetSetting, putUpdateSetting } from '@/api/User'; import MemberManage from './memberManage'; import LoginHistory from './loginHistory'; import { message } from '@c-x/ui'; +import DingingLoginSettingModal from './dingdingLoginSettingModal'; const StyledCard = styled(Card)({ display: 'flex', alignItems: 'center', justifyContent: 'space-between', + boxShadow: + '0px 0px 10px 0px rgba(68, 80, 91, 0.1), 0px 0px 2px 0px rgba(68, 80, 91, 0.1)', }); const StyledLabel = styled('div')(({ theme }) => ({ @@ -20,60 +34,143 @@ const StyledLabel = styled('div')(({ theme }) => ({ })); const User = () => { + const [dingdingLoginSettingModalOpen, setDingdingLoginSettingModalOpen] = + useState(false); + const [dingdingCheck, setDingdingCheck] = useState(false); const { data = { enable_sso: false, force_two_factor_auth: false, disable_password_login: false, + enable_dingtalk_oauth: false, }, refresh, - } = useRequest(() => getGetSetting()); - const { run: updateSetting } = useRequest(putUpdateSetting, { + } = useRequest(getGetSetting, { + onSuccess: (data) => { + setDingdingCheck(data.enable_dingtalk_oauth!); + }, + }); + + const { runAsync: updateSetting } = useRequest(putUpdateSetting, { manual: true, onSuccess: () => { refresh(); message.success('设置更新成功'); }, }); + + const onDisabledDingdingLogin = () => { + Modal.confirm({ + title: '提示', + content: '确定要关闭钉钉登录吗?', + onOk: () => { + updateSetting({ enable_dingtalk_oauth: false }).then(() => { + refresh(); + }); + }, + }); + }; + return ( - - - 单点登录 - updateSetting({ enable_sso: e.target.checked })} - /> - - - - - 强制成员启用两步认证 - { - console.log(e.target.checked); - updateSetting({ force_two_factor_auth: e.target.checked }); - }} - /> - + + + + 强制成员启用两步认证 + { + updateSetting({ force_two_factor_auth: e.target.checked }); + }} + /> + + + + + 禁止成员使用密码登录 + + updateSetting({ disable_password_login: e.target.checked }) + } + /> + + - - - 禁止成员使用密码登录 - - updateSetting({ disable_password_login: e.target.checked }) - } - /> - + + + + 第三方登录 + + + + + + + + setDingdingLoginSettingModalOpen(false)} + onOk={() => { + refresh(); + }} + /> ); }; diff --git a/ui/src/pages/user/inviteUserModal.tsx b/ui/src/pages/user/inviteUserModal.tsx index 2aa91e69..669fd14c 100644 --- a/ui/src/pages/user/inviteUserModal.tsx +++ b/ui/src/pages/user/inviteUserModal.tsx @@ -17,7 +17,7 @@ const InviteUserModal = ({ const { loading, refresh } = useRequest(getInvite, { manual: true, onSuccess: (data) => { - setInviteUrl(location.origin + '/invite/' + data?.code); + setInviteUrl(location.origin + '/invite/' + data?.code + '/1'); }, }); diff --git a/ui/src/router.tsx b/ui/src/router.tsx index f5f9fd17..cc50378e 100644 --- a/ui/src/router.tsx +++ b/ui/src/router.tsx @@ -80,7 +80,7 @@ const routerConfig = [ ], }, { - path: '/invite/:id', + path: '/invite/:id/:step?', element: , }, { diff --git a/ui/src/theme.ts b/ui/src/theme.ts index f8ca12b2..de7016a8 100644 --- a/ui/src/theme.ts +++ b/ui/src/theme.ts @@ -54,7 +54,7 @@ const lightTheme = createTheme( auxiliary: 'rgba(33,34,45, 0.5)', disabled: 'rgba(33,34,45, 0.2)', }, - divider: '#ECEEF1', + // divider: '#ECEEF1', }, shadows: [ ...defaultTheme.shadows.slice(0, 8),