From 0ddcdf93ec840535cc177035616e96d74e7e6634 Mon Sep 17 00:00:00 2001 From: zhaodongsheng Date: Tue, 22 Oct 2024 13:58:58 +0800 Subject: [PATCH] =?UTF-8?q?(feat)Support=20for=20managing=20large=20models?= =?UTF-8?q?=20with=20Dify=20#1830;2=E3=80=81add=20user=20access=20token;?= =?UTF-8?q?=20#1829;=203=E3=80=81support=20change=20password=20#1824=20(#1?= =?UTF-8?q?839)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../authentication/adaptor/UserAdaptor.java | 13 ++ .../api/authentication/pojo/UserToken.java | 20 ++ .../authentication/request/UserTokenReq.java | 15 ++ .../authentication/service/UserService.java | 13 ++ .../adaptor/DefaultUserAdaptor.java | 134 +++++++++++- .../AuthenticationInterceptor.java | 3 +- .../DefaultAuthenticationInterceptor.java | 5 +- .../persistence/dataobject/UserTokenDO.java | 25 +++ .../persistence/mapper/UserDOMapper.java | 2 + .../persistence/mapper/UserTokenDOMapper.java | 11 + .../repository/UserRepository.java | 13 ++ .../repository/impl/UserRepositoryImpl.java | 42 +++- .../authentication/rest/UserController.java | 27 +++ .../service/UserServiceImpl.java | 31 +++ .../strategy/HttpHeaderUserStrategy.java | 5 +- .../authentication/utils/TokenService.java | 87 ++++++-- .../main/resources/mapper/UserDOMapper.xml | 25 +++ .../resources/mapper/UserTokenDOMapper.xml | 14 ++ .../common/pojo/ChatModelParameters.java | 23 ++- .../supersonic/common/util/DifyClient.java | 85 ++++++++ .../supersonic/common/util/DifyRequest.java | 19 ++ .../supersonic/common/util/DifyResult.java | 13 ++ .../supersonic/common/util/HttpUtils.java | 170 +++++++++++++++ .../model/dify/DifyAiChatModel.java | 95 +++++++++ .../provider/DifyModelFactory.java | 41 ++++ .../src/main/resources/db/schema-h2.sql | 16 ++ .../src/main/resources/db/schema-mysql.sql | 16 ++ webapp/packages/supersonic-fe/config/proxy.ts | 2 +- .../RightContent/AccessTokensModal.tsx | 195 ++++++++++++++++++ .../RightContent/AvatarDropdown.tsx | 58 +++++- .../RightContent/ChangePasswordModal.tsx | 121 +++++++++++ .../supersonic-fe/src/services/API.d.ts | 17 ++ .../supersonic-fe/src/services/user.ts | 28 +++ .../packages/supersonic-fe/src/utils/utils.ts | 2 +- 34 files changed, 1341 insertions(+), 45 deletions(-) create mode 100644 auth/api/src/main/java/com/tencent/supersonic/auth/api/authentication/pojo/UserToken.java create mode 100644 auth/api/src/main/java/com/tencent/supersonic/auth/api/authentication/request/UserTokenReq.java create mode 100644 auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/persistence/dataobject/UserTokenDO.java create mode 100644 auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/persistence/mapper/UserTokenDOMapper.java create mode 100644 auth/authentication/src/main/resources/mapper/UserTokenDOMapper.xml create mode 100644 common/src/main/java/com/tencent/supersonic/common/util/DifyClient.java create mode 100644 common/src/main/java/com/tencent/supersonic/common/util/DifyRequest.java create mode 100644 common/src/main/java/com/tencent/supersonic/common/util/DifyResult.java create mode 100644 common/src/main/java/com/tencent/supersonic/common/util/HttpUtils.java create mode 100644 common/src/main/java/dev/langchain4j/model/dify/DifyAiChatModel.java create mode 100644 common/src/main/java/dev/langchain4j/provider/DifyModelFactory.java create mode 100644 webapp/packages/supersonic-fe/src/components/RightContent/AccessTokensModal.tsx create mode 100644 webapp/packages/supersonic-fe/src/components/RightContent/ChangePasswordModal.tsx diff --git a/auth/api/src/main/java/com/tencent/supersonic/auth/api/authentication/adaptor/UserAdaptor.java b/auth/api/src/main/java/com/tencent/supersonic/auth/api/authentication/adaptor/UserAdaptor.java index b0fe9faa3..b5767d7e6 100644 --- a/auth/api/src/main/java/com/tencent/supersonic/auth/api/authentication/adaptor/UserAdaptor.java +++ b/auth/api/src/main/java/com/tencent/supersonic/auth/api/authentication/adaptor/UserAdaptor.java @@ -3,6 +3,7 @@ package com.tencent.supersonic.auth.api.authentication.adaptor; import javax.servlet.http.HttpServletRequest; import com.tencent.supersonic.auth.api.authentication.pojo.Organization; +import com.tencent.supersonic.auth.api.authentication.pojo.UserToken; import com.tencent.supersonic.auth.api.authentication.request.UserReq; import com.tencent.supersonic.common.pojo.User; @@ -27,4 +28,16 @@ public interface UserAdaptor { List getUserByOrg(String key); Set getUserAllOrgId(String userName); + + String getPassword(String userName); + + void resetPassword(String userName, String password, String newPassword); + + UserToken generateToken(String name, String userName, long expireTime); + + void deleteUserToken(Long id); + + UserToken getUserToken(Long id); + + List getUserTokens(String userName); } diff --git a/auth/api/src/main/java/com/tencent/supersonic/auth/api/authentication/pojo/UserToken.java b/auth/api/src/main/java/com/tencent/supersonic/auth/api/authentication/pojo/UserToken.java new file mode 100644 index 000000000..c25127561 --- /dev/null +++ b/auth/api/src/main/java/com/tencent/supersonic/auth/api/authentication/pojo/UserToken.java @@ -0,0 +1,20 @@ +package com.tencent.supersonic.auth.api.authentication.pojo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserToken { + private Integer id; + private String name; + private String userName; + private String token; + private Long expireTime; + private Date createDate; + private Date expireDate; +} diff --git a/auth/api/src/main/java/com/tencent/supersonic/auth/api/authentication/request/UserTokenReq.java b/auth/api/src/main/java/com/tencent/supersonic/auth/api/authentication/request/UserTokenReq.java new file mode 100644 index 000000000..5c04d47bd --- /dev/null +++ b/auth/api/src/main/java/com/tencent/supersonic/auth/api/authentication/request/UserTokenReq.java @@ -0,0 +1,15 @@ +package com.tencent.supersonic.auth.api.authentication.request; + +import javax.validation.constraints.NotBlank; + +import lombok.Data; + +@Data +public class UserTokenReq { + @NotBlank(message = "name can not be null") + private String name; + + @NotBlank(message = "expireTime can not be null") + private long expireTime; + +} diff --git a/auth/api/src/main/java/com/tencent/supersonic/auth/api/authentication/service/UserService.java b/auth/api/src/main/java/com/tencent/supersonic/auth/api/authentication/service/UserService.java index fde397d3a..368059b6b 100644 --- a/auth/api/src/main/java/com/tencent/supersonic/auth/api/authentication/service/UserService.java +++ b/auth/api/src/main/java/com/tencent/supersonic/auth/api/authentication/service/UserService.java @@ -4,6 +4,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.tencent.supersonic.auth.api.authentication.pojo.Organization; +import com.tencent.supersonic.auth.api.authentication.pojo.UserToken; import com.tencent.supersonic.auth.api.authentication.request.UserReq; import com.tencent.supersonic.common.pojo.User; @@ -30,4 +31,16 @@ public interface UserService { List getUserByOrg(String key); List getOrganizationTree(); + + String getPassword(String userName); + + void resetPassword(String userName, String password, String newPassword); + + UserToken generateToken(String name, String userName, long expireTime); + + List getUserTokens(String userName); + + UserToken getUserToken(Long id); + + void deleteUserToken(Long id); } diff --git a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/adaptor/DefaultUserAdaptor.java b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/adaptor/DefaultUserAdaptor.java index b14c68a78..cb134e5a3 100644 --- a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/adaptor/DefaultUserAdaptor.java +++ b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/adaptor/DefaultUserAdaptor.java @@ -1,12 +1,16 @@ package com.tencent.supersonic.auth.authentication.adaptor; +import javax.servlet.http.HttpServletRequest; + import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.tencent.supersonic.auth.api.authentication.adaptor.UserAdaptor; import com.tencent.supersonic.auth.api.authentication.pojo.Organization; +import com.tencent.supersonic.auth.api.authentication.pojo.UserToken; import com.tencent.supersonic.auth.api.authentication.pojo.UserWithPassword; import com.tencent.supersonic.auth.api.authentication.request.UserReq; import com.tencent.supersonic.auth.authentication.persistence.dataobject.UserDO; +import com.tencent.supersonic.auth.authentication.persistence.dataobject.UserTokenDO; import com.tencent.supersonic.auth.authentication.persistence.repository.UserRepository; import com.tencent.supersonic.auth.authentication.utils.TokenService; import com.tencent.supersonic.common.pojo.User; @@ -15,8 +19,8 @@ import com.tencent.supersonic.common.util.ContextUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; -import javax.servlet.http.HttpServletRequest; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -106,6 +110,68 @@ public class DefaultUserAdaptor implements UserAdaptor { } } + @Override + public String getPassword(String userName) { + UserDO userDO = getUser(userName); + if (userDO == null) { + throw new RuntimeException("user not exist,please register"); + } + return userDO.getPassword(); + } + + @Override + public void resetPassword(String userName, String password, String newPassword) { + UserRepository userRepository = ContextUtils.getBean(UserRepository.class); + Optional userDOOptional = Optional.ofNullable(getUser(userName)); + + UserDO userDO = userDOOptional + .orElseThrow(() -> new RuntimeException("User does not exist, please register")); + + try { + validateOldPassword(userDO, password); + updatePassword(userDO, newPassword, userRepository); + } catch (PasswordEncryptionException e) { + throw new RuntimeException("Password encryption error, please try again", e); + } + } + + + private void validateOldPassword(UserDO userDO, String password) + throws PasswordEncryptionException { + String oldPassword = encryptPassword(password, userDO.getSalt()); + if (!userDO.getPassword().equals(oldPassword)) { + throw new RuntimeException("Old password is not correct, please try again"); + } + } + + private void updatePassword(UserDO userDO, String newPassword, UserRepository userRepository) + throws PasswordEncryptionException { + try { + byte[] salt = AESEncryptionUtil.generateSalt(userDO.getName()); + userDO.setSalt(AESEncryptionUtil.getStringFromBytes(salt)); + userDO.setPassword(AESEncryptionUtil.encrypt(newPassword, salt)); + userRepository.updateUser(userDO); + } catch (Exception e) { + throw new PasswordEncryptionException("Error encrypting password", e); + } + + } + + private String encryptPassword(String password, String salt) + throws PasswordEncryptionException { + try { + return AESEncryptionUtil.encrypt(password, AESEncryptionUtil.getBytesFromString(salt)); + } catch (Exception e) { + throw new PasswordEncryptionException("Error encrypting password", e); + } + } + + public static class PasswordEncryptionException extends Exception { + public PasswordEncryptionException(String message, Throwable cause) { + super(message, cause); + } + } + private UserWithPassword getUserWithPassword(UserReq userReq) { UserDO userDO = getUser(userReq.getName()); if (userDO == null) { @@ -136,4 +202,70 @@ public class DefaultUserAdaptor implements UserAdaptor { public Set getUserAllOrgId(String userName) { return Sets.newHashSet(); } + + @Override + public UserToken generateToken(String name, String userName, long expireTime) { + TokenService tokenService = ContextUtils.getBean(TokenService.class); + UserDO userDO = getUser(userName); + if (userDO == null) { + throw new RuntimeException("user not exist,please register"); + } + UserWithPassword userWithPassword = + new UserWithPassword(userDO.getId(), userDO.getName(), userDO.getDisplayName(), + userDO.getEmail(), userDO.getPassword(), userDO.getIsAdmin()); + + String token = + tokenService.generateToken(UserWithPassword.convert(userWithPassword), expireTime); + UserTokenDO userTokenDO = saveUserToken(name, userName, token, expireTime); + return convertUserToken(userTokenDO); + } + + @Override + public void deleteUserToken(Long id) { + UserRepository userRepository = ContextUtils.getBean(UserRepository.class); + userRepository.deleteUserToken(id); + } + + @Override + public UserToken getUserToken(Long id) { + UserRepository userRepository = ContextUtils.getBean(UserRepository.class); + return convertUserToken(userRepository.getUserToken(id)); + } + + @Override + public List getUserTokens(String userName) { + UserRepository userRepository = ContextUtils.getBean(UserRepository.class); + List userTokens = userRepository.getUserTokenListByName(userName).stream() + .map(this::convertUserToken).collect(Collectors.toList()); + return userTokens; + } + + private UserTokenDO saveUserToken(String tokenName, String userName, String token, + long expireTime) { + UserTokenDO userTokenDO = new UserTokenDO(); + userTokenDO.setName(tokenName); + userTokenDO.setUserName(userName); + userTokenDO.setToken(token); + userTokenDO.setExpireTime(expireTime); + userTokenDO.setCreateTime(new java.util.Date()); + userTokenDO.setCreateBy(userName); + userTokenDO.setUpdateBy(userName); + userTokenDO.setExpireDateTime(new java.util.Date(System.currentTimeMillis() + expireTime)); + UserRepository userRepository = ContextUtils.getBean(UserRepository.class); + userRepository.addUserToken(userTokenDO); + + return userTokenDO; + } + + private UserToken convertUserToken(UserTokenDO userTokenDO) { + UserToken userToken = new UserToken(); + userToken.setId(userTokenDO.getId()); + userToken.setName(userTokenDO.getName()); + userToken.setUserName(userTokenDO.getUserName()); + userToken.setToken(userTokenDO.getToken()); + userToken.setExpireTime(userTokenDO.getExpireTime()); + userToken.setCreateDate(userTokenDO.getCreateTime()); + userToken.setExpireDate(userTokenDO.getExpireDateTime()); + return userToken; + } } diff --git a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/interceptor/AuthenticationInterceptor.java b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/interceptor/AuthenticationInterceptor.java index 3bf482ac7..e34b69e0a 100644 --- a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/interceptor/AuthenticationInterceptor.java +++ b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/interceptor/AuthenticationInterceptor.java @@ -1,5 +1,7 @@ package com.tencent.supersonic.auth.authentication.interceptor; +import javax.servlet.http.HttpServletRequest; + import com.tencent.supersonic.auth.api.authentication.config.AuthenticationConfig; import com.tencent.supersonic.auth.api.authentication.constant.UserConstants; import com.tencent.supersonic.auth.authentication.service.UserServiceImpl; @@ -13,7 +15,6 @@ import org.springframework.util.CollectionUtils; import org.springframework.web.multipart.support.StandardMultipartHttpServletRequest; import org.springframework.web.servlet.HandlerInterceptor; -import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Field; import java.util.Arrays; import java.util.List; diff --git a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/interceptor/DefaultAuthenticationInterceptor.java b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/interceptor/DefaultAuthenticationInterceptor.java index adcd64ed7..385c390fc 100644 --- a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/interceptor/DefaultAuthenticationInterceptor.java +++ b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/interceptor/DefaultAuthenticationInterceptor.java @@ -1,5 +1,8 @@ package com.tencent.supersonic.auth.authentication.interceptor; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import com.tencent.supersonic.auth.api.authentication.annotation.AuthenticationIgnore; import com.tencent.supersonic.auth.api.authentication.config.AuthenticationConfig; import com.tencent.supersonic.auth.api.authentication.pojo.UserWithPassword; @@ -14,8 +17,6 @@ import io.jsonwebtoken.Claims; import lombok.extern.slf4j.Slf4j; import org.springframework.web.method.HandlerMethod; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method; import java.util.Optional; diff --git a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/persistence/dataobject/UserTokenDO.java b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/persistence/dataobject/UserTokenDO.java new file mode 100644 index 000000000..c590104b6 --- /dev/null +++ b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/persistence/dataobject/UserTokenDO.java @@ -0,0 +1,25 @@ +package com.tencent.supersonic.auth.authentication.persistence.dataobject; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.Date; + +@Data +@TableName("s2_user_token") +public class UserTokenDO { + @TableId(type = IdType.AUTO) + Integer id; + String name; + String userName; + Long expireTime; + String token; + String salt; + Date createTime; + Date updateTime; + String createBy; + String updateBy; + Date expireDateTime; +} diff --git a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/persistence/mapper/UserDOMapper.java b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/persistence/mapper/UserDOMapper.java index 6eb31b31d..7a2de09df 100644 --- a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/persistence/mapper/UserDOMapper.java +++ b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/persistence/mapper/UserDOMapper.java @@ -14,4 +14,6 @@ public interface UserDOMapper { /** @mbg.generated */ List selectByExample(UserDOExample example); + + void updateByPrimaryKey(UserDO userDO); } diff --git a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/persistence/mapper/UserTokenDOMapper.java b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/persistence/mapper/UserTokenDOMapper.java new file mode 100644 index 000000000..09e1f7bc5 --- /dev/null +++ b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/persistence/mapper/UserTokenDOMapper.java @@ -0,0 +1,11 @@ +package com.tencent.supersonic.auth.authentication.persistence.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.tencent.supersonic.auth.authentication.persistence.dataobject.UserTokenDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface UserTokenDOMapper extends BaseMapper { + +} diff --git a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/persistence/repository/UserRepository.java b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/persistence/repository/UserRepository.java index cb8e332af..2bf1727cf 100644 --- a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/persistence/repository/UserRepository.java +++ b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/persistence/repository/UserRepository.java @@ -1,6 +1,7 @@ package com.tencent.supersonic.auth.authentication.persistence.repository; import com.tencent.supersonic.auth.authentication.persistence.dataobject.UserDO; +import com.tencent.supersonic.auth.authentication.persistence.dataobject.UserTokenDO; import java.util.List; @@ -10,5 +11,17 @@ public interface UserRepository { void addUser(UserDO userDO); + List getUserTokenListByName(String userName); + UserDO getUser(String name); + + void updateUser(UserDO userDO); + + void addUserToken(UserTokenDO userTokenDO); + + UserTokenDO getUserToken(Long tokenId); + + void deleteUserTokenByName(String userName); + + void deleteUserToken(Long tokenId); } diff --git a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/persistence/repository/impl/UserRepositoryImpl.java b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/persistence/repository/impl/UserRepositoryImpl.java index e4be6fc72..1549a1db6 100644 --- a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/persistence/repository/impl/UserRepositoryImpl.java +++ b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/persistence/repository/impl/UserRepositoryImpl.java @@ -1,8 +1,11 @@ package com.tencent.supersonic.auth.authentication.persistence.repository.impl; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.tencent.supersonic.auth.authentication.persistence.dataobject.UserDO; import com.tencent.supersonic.auth.authentication.persistence.dataobject.UserDOExample; +import com.tencent.supersonic.auth.authentication.persistence.dataobject.UserTokenDO; import com.tencent.supersonic.auth.authentication.persistence.mapper.UserDOMapper; +import com.tencent.supersonic.auth.authentication.persistence.mapper.UserTokenDOMapper; import com.tencent.supersonic.auth.authentication.persistence.repository.UserRepository; import org.springframework.stereotype.Component; @@ -14,8 +17,11 @@ public class UserRepositoryImpl implements UserRepository { private UserDOMapper userDOMapper; - public UserRepositoryImpl(UserDOMapper userDOMapper) { + private UserTokenDOMapper userTokenDOMapper; + + public UserRepositoryImpl(UserDOMapper userDOMapper, UserTokenDOMapper userTokenDOMapper) { this.userDOMapper = userDOMapper; + this.userTokenDOMapper = userTokenDOMapper; } @Override @@ -23,6 +29,11 @@ public class UserRepositoryImpl implements UserRepository { return userDOMapper.selectByExample(new UserDOExample()); } + @Override + public void updateUser(UserDO userDO) { + userDOMapper.updateByPrimaryKey(userDO); + } + @Override public void addUser(UserDO userDO) { userDOMapper.insert(userDO); @@ -36,4 +47,33 @@ public class UserRepositoryImpl implements UserRepository { Optional userDOOptional = userDOS.stream().findFirst(); return userDOOptional.orElse(null); } + + @Override + public List getUserTokenListByName(String userName) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("user_name", userName); + return userTokenDOMapper.selectList(queryWrapper); + } + + @Override + public void addUserToken(UserTokenDO userTokenDO) { + userTokenDOMapper.insert(userTokenDO); + } + + @Override + public UserTokenDO getUserToken(Long tokenId) { + return userTokenDOMapper.selectById(tokenId); + } + + @Override + public void deleteUserTokenByName(String userName) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("user_name", userName); + userTokenDOMapper.delete(queryWrapper); + } + + @Override + public void deleteUserToken(Long tokenId) { + userTokenDOMapper.deleteById(tokenId); + } } diff --git a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/rest/UserController.java b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/rest/UserController.java index 327feaf42..3f7076acd 100644 --- a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/rest/UserController.java +++ b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/rest/UserController.java @@ -4,7 +4,9 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.tencent.supersonic.auth.api.authentication.pojo.Organization; +import com.tencent.supersonic.auth.api.authentication.pojo.UserToken; import com.tencent.supersonic.auth.api.authentication.request.UserReq; +import com.tencent.supersonic.auth.api.authentication.request.UserTokenReq; import com.tencent.supersonic.auth.api.authentication.service.UserService; import com.tencent.supersonic.common.pojo.User; import lombok.extern.slf4j.Slf4j; @@ -13,6 +15,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; @@ -69,4 +72,28 @@ public class UserController { public String login(@RequestBody UserReq userCmd, HttpServletRequest request) { return userService.login(userCmd, request); } + + @PostMapping("/generateToken") + public UserToken generateToken(@RequestBody UserTokenReq userTokenReq, + HttpServletRequest request, HttpServletResponse response) { + User user = userService.getCurrentUser(request, response); + return userService.generateToken(userTokenReq.getName(), user.getName(), + userTokenReq.getExpireTime()); + } + + @GetMapping("/getUserTokens") + public List getUserTokens(HttpServletRequest request, HttpServletResponse response) { + User user = userService.getCurrentUser(request, response); + return userService.getUserTokens(user.getName()); + } + + @GetMapping("/getUserToken") + public UserToken getUserToken(@RequestParam(name = "tokenId") Long tokenId) { + return userService.getUserToken(tokenId); + } + + @PostMapping("/deleteUserToken") + public void deleteUserToken(@RequestParam(name = "tokenId") Long tokenId) { + userService.deleteUserToken(tokenId); + } } diff --git a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/service/UserServiceImpl.java b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/service/UserServiceImpl.java index d3c7bddc3..a10978b0c 100644 --- a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/service/UserServiceImpl.java +++ b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/service/UserServiceImpl.java @@ -4,6 +4,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.tencent.supersonic.auth.api.authentication.pojo.Organization; +import com.tencent.supersonic.auth.api.authentication.pojo.UserToken; import com.tencent.supersonic.auth.api.authentication.request.UserReq; import com.tencent.supersonic.auth.api.authentication.service.UserService; import com.tencent.supersonic.auth.api.authentication.utils.UserHolder; @@ -79,4 +80,34 @@ public class UserServiceImpl implements UserService { public String login(UserReq userReq, String appKey) { return ComponentFactory.getUserAdaptor().login(userReq, appKey); } + + @Override + public String getPassword(String userName) { + return ComponentFactory.getUserAdaptor().getPassword(userName); + } + + @Override + public void resetPassword(String userName, String password, String newPassword) { + ComponentFactory.getUserAdaptor().resetPassword(userName, password, newPassword); + } + + @Override + public UserToken generateToken(String name, String userName, long expireTime) { + return ComponentFactory.getUserAdaptor().generateToken(name, userName, expireTime); + } + + @Override + public List getUserTokens(String userName) { + return ComponentFactory.getUserAdaptor().getUserTokens(userName); + } + + @Override + public UserToken getUserToken(Long id) { + return ComponentFactory.getUserAdaptor().getUserToken(id); + } + + @Override + public void deleteUserToken(Long id) { + ComponentFactory.getUserAdaptor().deleteUserToken(id); + } } diff --git a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/strategy/HttpHeaderUserStrategy.java b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/strategy/HttpHeaderUserStrategy.java index 446143db6..970392724 100644 --- a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/strategy/HttpHeaderUserStrategy.java +++ b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/strategy/HttpHeaderUserStrategy.java @@ -1,5 +1,8 @@ package com.tencent.supersonic.auth.authentication.strategy; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import com.tencent.supersonic.auth.api.authentication.constant.UserConstants; import com.tencent.supersonic.auth.api.authentication.service.UserStrategy; import com.tencent.supersonic.auth.authentication.utils.TokenService; @@ -7,8 +10,6 @@ import com.tencent.supersonic.common.pojo.User; import io.jsonwebtoken.Claims; import org.springframework.stereotype.Service; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.util.Optional; @Service diff --git a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/utils/TokenService.java b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/utils/TokenService.java index 7e01b3357..e4e568de3 100644 --- a/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/utils/TokenService.java +++ b/auth/authentication/src/main/java/com/tencent/supersonic/auth/authentication/utils/TokenService.java @@ -1,6 +1,10 @@ package com.tencent.supersonic.auth.authentication.utils; +import javax.crypto.spec.SecretKeySpec; +import javax.servlet.http.HttpServletRequest; + import com.tencent.supersonic.auth.api.authentication.config.AuthenticationConfig; +import com.tencent.supersonic.auth.api.authentication.pojo.UserWithPassword; import com.tencent.supersonic.common.pojo.exception.AccessException; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; @@ -9,14 +13,11 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; -import javax.crypto.spec.SecretKeySpec; -import javax.servlet.http.HttpServletRequest; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.Map; import java.util.Optional; -import static com.tencent.supersonic.auth.api.authentication.constant.UserConstants.TOKEN_CREATE_TIME; import static com.tencent.supersonic.auth.api.authentication.constant.UserConstants.TOKEN_PREFIX; import static com.tencent.supersonic.auth.api.authentication.constant.UserConstants.TOKEN_USER_NAME; @@ -32,33 +33,40 @@ public class TokenService { public String generateToken(Map claims, HttpServletRequest request) { String appKey = getAppKey(request); - return generateToken(claims, appKey); + long expiration = System.currentTimeMillis() + authenticationConfig.getTokenTimeout(); + return generateToken(claims, appKey, expiration); + } + + public String generateToken(Map claims, long expiration) { + String appKey = authenticationConfig.getTokenDefaultAppKey(); + long exp = System.currentTimeMillis() + expiration; + return generateToken(claims, appKey, exp); } public String generateToken(Map claims, String appKey) { - return toTokenString(claims, appKey); + long expiration = System.currentTimeMillis() + authenticationConfig.getTokenTimeout(); + return toTokenString(claims, appKey, expiration); } - private String toTokenString(Map claims, String appKey) { - Long tokenTimeout = authenticationConfig.getTokenTimeout(); - long expiration = Long.parseLong(claims.get(TOKEN_CREATE_TIME) + "") + tokenTimeout; - Date expirationDate = new Date(expiration); - String tokenSecret = getTokenSecret(appKey); - - return Jwts.builder().setClaims(claims).setSubject(claims.get(TOKEN_USER_NAME).toString()) - .setExpiration(expirationDate) - .signWith(new SecretKeySpec(tokenSecret.getBytes(StandardCharsets.UTF_8), - SignatureAlgorithm.HS512.getJcaName()), SignatureAlgorithm.HS512) - .compact(); + public String generateToken(Map claims, String appKey, long expiration) { + return toTokenString(claims, appKey, expiration); } - private String getTokenSecret(String appKey) { - Map appKeyToSecretMap = authenticationConfig.getAppKeyToSecretMap(); - String secret = appKeyToSecretMap.get(appKey); - if (StringUtils.isBlank(secret)) { - throw new AccessException("get secret from appKey failed :" + appKey); + public String generateAppUserToken(HttpServletRequest request) { + String appName = request.getHeader("AppId"); + if (StringUtils.isBlank(appName)) { + String message = "AppId is blank, get app_user failed"; + log.warn("{}, uri: {}", message, request.getServletPath()); + throw new AccessException(message); } - return secret; + + UserWithPassword appUser = new UserWithPassword(appName); + appUser.setId(1L); + appUser.setName(appName); + appUser.setPassword("c3VwZXJzb25pY0BiaWNvbdktJJYWw6A3rEmBUPzbn/6DNeYnD+y3mAwDKEMS3KVT"); + appUser.setDisplayName(appName); + appUser.setIsAdmin(0); + return generateToken(UserWithPassword.convert(appUser), request); } public Optional getClaims(HttpServletRequest request) { @@ -67,6 +75,17 @@ public class TokenService { return getClaims(token, appKey); } + private Optional getClaims(String token, HttpServletRequest request) { + Optional claims; + try { + String appKey = getAppKey(request); + claims = getClaims(token, appKey); + } catch (Exception e) { + throw new AccessException("parse user info from token failed :" + token); + } + return claims; + } + public Optional getClaims(String token, String appKey) { try { String tokenSecret = getTokenSecret(appKey); @@ -86,6 +105,26 @@ public class TokenService { : token.trim(); } + private String toTokenString(Map claims, String appKey, long expiration) { + Date expirationDate = new Date(expiration); + String tokenSecret = getTokenSecret(appKey); + + return Jwts.builder().setClaims(claims).setSubject(claims.get(TOKEN_USER_NAME).toString()) + .setExpiration(expirationDate) + .signWith(new SecretKeySpec(tokenSecret.getBytes(StandardCharsets.UTF_8), + SignatureAlgorithm.HS512.getJcaName()), SignatureAlgorithm.HS512) + .compact(); + } + + private String getTokenSecret(String appKey) { + Map appKeyToSecretMap = authenticationConfig.getAppKeyToSecretMap(); + String secret = appKeyToSecretMap.get(appKey); + if (StringUtils.isBlank(secret)) { + throw new AccessException("get secret from appKey failed :" + appKey); + } + return secret; + } + public String getAppKey(HttpServletRequest request) { String appKey = request.getHeader(authenticationConfig.getTokenHttpHeaderAppKey()); if (StringUtils.isBlank(appKey)) { @@ -93,4 +132,8 @@ public class TokenService { } return appKey; } + + public String getDefaultAppKey() { + return authenticationConfig.getTokenDefaultAppKey(); + } } diff --git a/auth/authentication/src/main/resources/mapper/UserDOMapper.xml b/auth/authentication/src/main/resources/mapper/UserDOMapper.xml index 783906d5d..acb7663d5 100644 --- a/auth/authentication/src/main/resources/mapper/UserDOMapper.xml +++ b/auth/authentication/src/main/resources/mapper/UserDOMapper.xml @@ -122,4 +122,29 @@ + + + update s2_user + + + name = #{name,jdbcType=VARCHAR}, + + + password = #{password,jdbcType=VARCHAR}, + + + salt = #{salt,jdbcType=VARCHAR}, + + + display_name = #{displayName,jdbcType=VARCHAR}, + + + email = #{email,jdbcType=VARCHAR}, + + + is_admin = #{isAdmin,jdbcType=INTEGER}, + + + where id = #{id,jdbcType=BIGINT} + \ No newline at end of file diff --git a/auth/authentication/src/main/resources/mapper/UserTokenDOMapper.xml b/auth/authentication/src/main/resources/mapper/UserTokenDOMapper.xml new file mode 100644 index 000000000..d8757c2cd --- /dev/null +++ b/auth/authentication/src/main/resources/mapper/UserTokenDOMapper.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common/src/main/java/com/tencent/supersonic/common/pojo/ChatModelParameters.java b/common/src/main/java/com/tencent/supersonic/common/pojo/ChatModelParameters.java index a0bf54af0..fcf5a2535 100644 --- a/common/src/main/java/com/tencent/supersonic/common/pojo/ChatModelParameters.java +++ b/common/src/main/java/com/tencent/supersonic/common/pojo/ChatModelParameters.java @@ -2,8 +2,15 @@ package com.tencent.supersonic.common.pojo; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; -import com.tencent.supersonic.common.pojo.Parameter; -import dev.langchain4j.provider.*; +import dev.langchain4j.provider.AzureModelFactory; +import dev.langchain4j.provider.DashscopeModelFactory; +import dev.langchain4j.provider.DifyModelFactory; +import dev.langchain4j.provider.LocalAiModelFactory; +import dev.langchain4j.provider.ModelProvider; +import dev.langchain4j.provider.OllamaModelFactory; +import dev.langchain4j.provider.OpenAiModelFactory; +import dev.langchain4j.provider.QianfanModelFactory; +import dev.langchain4j.provider.ZhipuModelFactory; import java.util.ArrayList; import java.util.List; @@ -52,7 +59,7 @@ public class ChatModelParameters { return Lists.newArrayList(OpenAiModelFactory.PROVIDER, OllamaModelFactory.PROVIDER, QianfanModelFactory.PROVIDER, ZhipuModelFactory.PROVIDER, LocalAiModelFactory.PROVIDER, DashscopeModelFactory.PROVIDER, - AzureModelFactory.PROVIDER); + AzureModelFactory.PROVIDER, DifyModelFactory.PROVIDER); } private static List getBaseUrlDependency() { @@ -63,20 +70,23 @@ public class ChatModelParameters { QianfanModelFactory.PROVIDER, QianfanModelFactory.DEFAULT_BASE_URL, ZhipuModelFactory.PROVIDER, ZhipuModelFactory.DEFAULT_BASE_URL, LocalAiModelFactory.PROVIDER, LocalAiModelFactory.DEFAULT_BASE_URL, - DashscopeModelFactory.PROVIDER, DashscopeModelFactory.DEFAULT_BASE_URL)); + DashscopeModelFactory.PROVIDER, DashscopeModelFactory.DEFAULT_BASE_URL, + DifyModelFactory.PROVIDER, DifyModelFactory.DEFAULT_BASE_URL)); } private static List getApiKeyDependency() { return getDependency(CHAT_MODEL_PROVIDER.getName(), Lists.newArrayList(OpenAiModelFactory.PROVIDER, QianfanModelFactory.PROVIDER, ZhipuModelFactory.PROVIDER, LocalAiModelFactory.PROVIDER, - AzureModelFactory.PROVIDER, DashscopeModelFactory.PROVIDER), + AzureModelFactory.PROVIDER, DashscopeModelFactory.PROVIDER, + DifyModelFactory.PROVIDER), ImmutableMap.of(OpenAiModelFactory.PROVIDER, ModelProvider.DEMO_CHAT_MODEL.getApiKey(), QianfanModelFactory.PROVIDER, ModelProvider.DEMO_CHAT_MODEL.getApiKey(), ZhipuModelFactory.PROVIDER, ModelProvider.DEMO_CHAT_MODEL.getApiKey(), LocalAiModelFactory.PROVIDER, ModelProvider.DEMO_CHAT_MODEL.getApiKey(), AzureModelFactory.PROVIDER, ModelProvider.DEMO_CHAT_MODEL.getApiKey(), DashscopeModelFactory.PROVIDER, + ModelProvider.DEMO_CHAT_MODEL.getApiKey(), DifyModelFactory.PROVIDER, ModelProvider.DEMO_CHAT_MODEL.getApiKey())); } @@ -88,7 +98,8 @@ public class ChatModelParameters { ZhipuModelFactory.PROVIDER, ZhipuModelFactory.DEFAULT_MODEL_NAME, LocalAiModelFactory.PROVIDER, LocalAiModelFactory.DEFAULT_MODEL_NAME, AzureModelFactory.PROVIDER, AzureModelFactory.DEFAULT_MODEL_NAME, - DashscopeModelFactory.PROVIDER, DashscopeModelFactory.DEFAULT_MODEL_NAME)); + DashscopeModelFactory.PROVIDER, DashscopeModelFactory.DEFAULT_MODEL_NAME, + DifyModelFactory.PROVIDER, DifyModelFactory.DEFAULT_MODEL_NAME)); } private static List getEndpointDependency() { diff --git a/common/src/main/java/com/tencent/supersonic/common/util/DifyClient.java b/common/src/main/java/com/tencent/supersonic/common/util/DifyClient.java new file mode 100644 index 000000000..b79dd5b5d --- /dev/null +++ b/common/src/main/java/com/tencent/supersonic/common/util/DifyClient.java @@ -0,0 +1,85 @@ +package com.tencent.supersonic.common.util; + +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Slf4j +public class DifyClient { + private static final String DEFAULT_USER = "zhaodongsheng"; + private static final String CONTENT_TYPE_JSON = "application/json"; + + private String difyURL; + private String difyKey; + + public DifyClient(String difyURL, String difyKey) { + this.difyURL = difyURL; + this.difyKey = difyKey; + } + + public DifyResult generate(String prompt) { + Map headers = defaultHeaders(); + DifyRequest request = new DifyRequest(); + request.setQuery(prompt); + request.setUser(DEFAULT_USER); + return sendRequest(request, headers); + } + + public DifyResult generate(String prompt, String user) { + Map headers = defaultHeaders(); + DifyRequest request = new DifyRequest(); + request.setQuery(prompt); + request.setUser(user); + return sendRequest(request, headers); + } + + public DifyResult generate(Map inputs, String queryText, String user, + String conversationId) { + Map headers = defaultHeaders(); + DifyRequest request = new DifyRequest(); + request.setInputs(inputs); + request.setQuery(queryText); + request.setUser(user); + if (conversationId != null && !conversationId.isEmpty()) { + request.setConversationId(conversationId); + } + return sendRequest(request, headers); + } + + public DifyResult sendRequest(DifyRequest request, Map headers) { + try { + log.debug("请求dify- header--->" + JsonUtil.toString(headers)); + log.debug("请求dify- conversionId--->" + JsonUtil.toString(request)); + return HttpUtils.post(difyURL, JsonUtil.toString(request), headers, DifyResult.class); + } catch (Exception e) { + log.error("请求dify失败---->" + e.getMessage()); + throw new RuntimeException(e); + } + } + + public String parseSQLResult(String sql) { + Pattern pattern = Pattern.compile("```(sql)?(.*)```", Pattern.DOTALL); + Matcher matcher = pattern.matcher(sql); + if (!matcher.find()) { + return sql.trim(); + } else { + return matcher.group(2).trim(); + } + } + + private Map defaultHeaders() { + Map headers = new HashMap<>(); + if (difyKey.contains("Bearer")) { + headers.put("Authorization", difyKey); + } else { + headers.put("Authorization", "Bearer " + difyKey); + } + + headers.put("Content-Type", CONTENT_TYPE_JSON); + return headers; + } + +} diff --git a/common/src/main/java/com/tencent/supersonic/common/util/DifyRequest.java b/common/src/main/java/com/tencent/supersonic/common/util/DifyRequest.java new file mode 100644 index 000000000..8af95c52f --- /dev/null +++ b/common/src/main/java/com/tencent/supersonic/common/util/DifyRequest.java @@ -0,0 +1,19 @@ +package com.tencent.supersonic.common.util; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.HashMap; +import java.util.Map; + +@Data +public class DifyRequest { + private String query; + private Map inputs = new HashMap<>(); + private String responseMode = "blocking"; + private String user; + @JsonProperty("conversation_id") + private String conversationId; + @JsonProperty("auto_generate_name") + private Boolean autoGenerateName = false; +} diff --git a/common/src/main/java/com/tencent/supersonic/common/util/DifyResult.java b/common/src/main/java/com/tencent/supersonic/common/util/DifyResult.java new file mode 100644 index 000000000..34d761aca --- /dev/null +++ b/common/src/main/java/com/tencent/supersonic/common/util/DifyResult.java @@ -0,0 +1,13 @@ +package com.tencent.supersonic.common.util; + +import lombok.Data; + +@Data +public class DifyResult { + private String event = ""; + private String taskId = ""; + private String conversationId = ""; + private String id = ""; + private String messageId = ""; + private String answer = ""; +} diff --git a/common/src/main/java/com/tencent/supersonic/common/util/HttpUtils.java b/common/src/main/java/com/tencent/supersonic/common/util/HttpUtils.java new file mode 100644 index 000000000..12095d931 --- /dev/null +++ b/common/src/main/java/com/tencent/supersonic/common/util/HttpUtils.java @@ -0,0 +1,170 @@ +package com.tencent.supersonic.common.util; + +import okhttp3.Dispatcher; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class HttpUtils { + + private static final Logger logger = LoggerFactory.getLogger(HttpUtils.class); + + // 重试参考:okhttp3.RealCall.getResponseWithInterceptorChain + private static final OkHttpClient client = new OkHttpClient.Builder() + .readTimeout(3, TimeUnit.MINUTES).retryOnConnectionFailure(true).build(); + + static { + Dispatcher dispatcher = client.dispatcher(); + dispatcher.setMaxRequestsPerHost(300); + dispatcher.setMaxRequests(200); + } + + public static Response execute(String url) throws IOException { + Request request = new Request.Builder().url(url).build(); + return client.newCall(request).execute(); + } + + public static String get(String url) throws IOException { + return doRequest(builder(url).build()); + } + + public static String get(String url, Map headers) throws IOException { + return doRequest(headerBuilder(url, headers).build()); + } + + public static String get(String url, Map headers, Map params) + throws IOException { + return doRequest(headerBuilder(url + buildUrlParams(params), headers).build()); + } + + public static T get(String url, Class classOfT) throws IOException { + return doRequest(builder(url).build(), classOfT); + } + + public static T get(String url, Map headers, Class classOfT) + throws IOException { + return doRequest(headerBuilder(url, headers).build(), classOfT); + } + + public static T get(String url, Map headers, Map params, + Class classOfT) throws IOException { + return doRequest(headerBuilder(url + buildUrlParams(params), headers).build(), classOfT); + } + + // public static T get(String url, TypeReference type) throws IOException { + // return doRequest(builder(url).build(), type); + // } + + // public static T get(String url, Map headers, TypeReference type) + // throws IOException { + // return doRequest(headerBuilder(url, headers).build(), type); + // } + + // public static T get(String url, Map headers, Map params, + // TypeReference type) throws IOException { + // return doRequest(headerBuilder(url + buildUrlParams(params), headers).build(), type); + // } + + public static String post(String url, Object body) throws IOException { + return doRequest(postRequest(url, body)); + } + + public static String post(String url, Object body, Map headers) + throws IOException { + return doRequest(postRequest(url, body, headers)); + } + + public static T post(String url, Object body, Class classOfT) throws IOException { + return doRequest(postRequest(url, body), classOfT); + } + + // public static T post(String url, Object body, TypeReference type) throws IOException { + // return doRequest(postRequest(url, body), type); + // } + + public static T post(String url, Object body, Map headers, + Class classOfT) throws IOException { + return doRequest(postRequest(url, body, headers), classOfT); + } + + // public static T post(String url, Object body, Map headers, + // TypeReference type) throws IOException { + // return doRequest(postRequest(url, body, headers), type); + // } + + private static Request postRequest(String url, Object body) { + return builder(url).post(buildRequestBody(body, null)).build(); + } + + private static Request postRequest(String url, Object body, Map headers) { + return headerBuilder(url, headers).post(buildRequestBody(body, headers)).build(); + } + + private static Request.Builder builder(String url) { + return new Request.Builder().url(url); + } + + private static Request.Builder headerBuilder(String url, Map headers) { + Request.Builder builder = new Request.Builder().url(url); + headers.forEach(builder::addHeader); + + return builder; + } + + private static T doRequest(Request request, Class classOfT) throws IOException { + return JsonUtil.toObject(doRequest(request), classOfT); + } + + // private static T doRequest(Request request, TypeReference type) throws IOException { + // return JsonUtil.toObject(doRequest(request), type); + // } + + private static String doRequest(Request request) throws IOException { + long beginTime = System.currentTimeMillis(); + + try { + Response response = client.newCall(request).execute(); + if (response.isSuccessful()) { + return response.body().string(); + } else { + throw new RuntimeException( + "Http请求失败[" + response.code() + "]:" + response.body().string() + "..."); + } + } finally { + logger.info("begin to request : {}, execute costs(ms) : {}", request.url(), + System.currentTimeMillis() - beginTime); + } + } + + private static RequestBody buildRequestBody(Object body, Map headers) { + if (headers != null && headers.containsKey("Content-Type")) { + String contentType = headers.get("Content-Type"); + return RequestBody.create(MediaType.parse(contentType), body.toString()); + } + + if (body instanceof String && ((String) body).contains("=")) { + return RequestBody.create(MediaType.parse("application/x-www-form-urlencoded"), + (String) body); + } + + return RequestBody.create(MediaType.parse("application/json"), JsonUtil.toString(body)); + } + + private static String buildUrlParams(Map params) { + if (params.isEmpty()) { + return ""; + } + + return "?" + params.entrySet().stream().map(it -> it.getKey() + "=" + it.getValue()) + .collect(Collectors.joining("&")); + } +} diff --git a/common/src/main/java/dev/langchain4j/model/dify/DifyAiChatModel.java b/common/src/main/java/dev/langchain4j/model/dify/DifyAiChatModel.java new file mode 100644 index 000000000..3660df811 --- /dev/null +++ b/common/src/main/java/dev/langchain4j/model/dify/DifyAiChatModel.java @@ -0,0 +1,95 @@ +package dev.langchain4j.model.dify; + +import com.tencent.supersonic.common.util.AESEncryptionUtil; +import com.tencent.supersonic.common.util.DifyClient; +import com.tencent.supersonic.common.util.DifyResult; +import dev.langchain4j.agent.tool.ToolSpecification; +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.model.chat.ChatLanguageModel; +import dev.langchain4j.model.output.Response; +import lombok.Builder; + +import java.util.List; + +import static dev.langchain4j.internal.Utils.getOrDefault; +import static dev.langchain4j.internal.Utils.isNullOrEmpty; +import static dev.langchain4j.internal.ValidationUtils.ensureNotEmpty; +import static java.util.Collections.singletonList; + +public class DifyAiChatModel implements ChatLanguageModel { + + private static final String CONTENT_TYPE_JSON = "application/json"; + + private final String baseUrl; + private final String apiKey; + + private final DifyClient difyClient; + private final Integer maxRetries; + private final Integer maxToken; + + private final String appName; + private final Double temperature; + private final Long timeOut; + + private String userName; + + @Builder + public DifyAiChatModel(String baseUrl, String apiKey, Integer maxRetries, Integer maxToken, + String modelName, Double temperature, Long timeOut) { + this.baseUrl = baseUrl; + this.maxRetries = getOrDefault(maxRetries, 3); + this.maxToken = getOrDefault(maxToken, 512); + try { + this.apiKey = AESEncryptionUtil.aesDecryptECB(apiKey); + } catch (Exception e) { + throw new RuntimeException(e); + } + this.appName = modelName; + this.temperature = temperature; + this.timeOut = timeOut; + this.difyClient = new DifyClient(this.baseUrl, this.apiKey); + } + + @Override + public String generate(String message) { + DifyResult difyResult = this.difyClient.generate(message, this.getUserName()); + return difyResult.getAnswer().toString(); + } + + @Override + public Response generate(List messages) { + return generate(messages, (ToolSpecification) null); + } + + @Override + public Response generate(List messages, + List toolSpecifications) { + ensureNotEmpty(messages, "messages"); + DifyResult difyResult = + this.difyClient.generate(messages.get(0).text(), this.getUserName()); + System.out.println(difyResult.toString()); + + if (!isNullOrEmpty(toolSpecifications)) { + // TODO + } + + return Response.from(AiMessage.from(difyResult.getAnswer())); + } + + @Override + public Response generate(List messages, + ToolSpecification toolSpecification) { + return generate(messages, + toolSpecification != null ? singletonList(toolSpecification) : null); + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getUserName() { + return null == userName ? "zhaodongsheng" : userName; + } + +} diff --git a/common/src/main/java/dev/langchain4j/provider/DifyModelFactory.java b/common/src/main/java/dev/langchain4j/provider/DifyModelFactory.java new file mode 100644 index 000000000..c1cf43d25 --- /dev/null +++ b/common/src/main/java/dev/langchain4j/provider/DifyModelFactory.java @@ -0,0 +1,41 @@ +package dev.langchain4j.provider; + +import com.tencent.supersonic.common.pojo.ChatModelConfig; +import com.tencent.supersonic.common.pojo.EmbeddingModelConfig; +import com.tencent.supersonic.common.util.AESEncryptionUtil; +import dev.langchain4j.model.chat.ChatLanguageModel; +import dev.langchain4j.model.dify.DifyAiChatModel; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.model.zhipu.ZhipuAiEmbeddingModel; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Service; + +@Service +public class DifyModelFactory implements ModelFactory, InitializingBean { + public static final String PROVIDER = "DIFY"; + + public static final String DEFAULT_BASE_URL = "https://dify.com/v1/chat-messages"; + public static final String DEFAULT_MODEL_NAME = "demo-预留-可不填写"; + public static final String DEFAULT_EMBEDDING_MODEL_NAME = "all-minilm"; + + @Override + public ChatLanguageModel createChatModel(ChatModelConfig modelConfig) { + return DifyAiChatModel.builder().baseUrl(modelConfig.getBaseUrl()) + .apiKey(AESEncryptionUtil.aesDecryptECB(modelConfig.getApiKey())) + .modelName(modelConfig.getModelName()).timeOut(modelConfig.getTimeOut()).build(); + } + + @Override + public EmbeddingModel createEmbeddingModel(EmbeddingModelConfig embeddingModelConfig) { + return ZhipuAiEmbeddingModel.builder().baseUrl(embeddingModelConfig.getBaseUrl()) + .apiKey(embeddingModelConfig.getApiKey()).model(embeddingModelConfig.getModelName()) + .maxRetries(embeddingModelConfig.getMaxRetries()) + .logRequests(embeddingModelConfig.getLogRequests()) + .logResponses(embeddingModelConfig.getLogResponses()).build(); + } + + @Override + public void afterPropertiesSet() { + ModelProvider.add(PROVIDER, this); + } +} diff --git a/launchers/standalone/src/main/resources/db/schema-h2.sql b/launchers/standalone/src/main/resources/db/schema-h2.sql index 4d3d6ec37..71ffa6ca9 100644 --- a/launchers/standalone/src/main/resources/db/schema-h2.sql +++ b/launchers/standalone/src/main/resources/db/schema-h2.sql @@ -682,3 +682,19 @@ CREATE TABLE IF NOT EXISTS `s2_term` ( PRIMARY KEY (`id`) ); COMMENT ON TABLE s2_term IS 'term info'; + +CREATE TABLE IF NOT EXISTS `s2_user_token` ( + `id` INT NOT NULL AUTO_INCREMENT, + `name` VARCHAR(255) NOT NULL, + `user_name` VARCHAR(255) NOT NULL, + `expire_time` INT NOT NULL, + `token` text NOT NULL, + `salt` VARCHAR(255) default NULL, + `create_time` DATETIME NOT NULL, + `create_by` VARCHAR(255) NOT NULL, + `update_time` DATETIME default NULL, + `update_by` VARCHAR(255) NOT NULL, + `expire_date_time` DATETIME NOT NULL, + PRIMARY KEY (`id`) + ); +COMMENT ON TABLE s2_user_token IS 'user token info'; diff --git a/launchers/standalone/src/main/resources/db/schema-mysql.sql b/launchers/standalone/src/main/resources/db/schema-mysql.sql index 646eb7d35..9b31b8bcd 100644 --- a/launchers/standalone/src/main/resources/db/schema-mysql.sql +++ b/launchers/standalone/src/main/resources/db/schema-mysql.sql @@ -603,3 +603,19 @@ CREATE TABLE IF NOT EXISTS `s2_term` ( `updated_by` varchar(100) DEFAULT NULL , PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='术语表'; + +CREATE TABLE `s2_user_token` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` VARCHAR(255) NOT NULL, + `user_name` VARCHAR(255) NOT NULL, + `expire_time` BIGINT(20) NOT NULL, + `token` text NOT NULL, + `salt` VARCHAR(255) default NULL, + `create_time` DATETIME NOT NULL, + `create_by` VARCHAR(255) NOT NULL, + `update_time` DATETIME default NULL, + `update_by` VARCHAR(255) NOT NULL, + `expire_date_time` DATETIME NOT NULL, + unique key name_username (`name`, `user_name`), + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin comment='用户令牌信息表'; diff --git a/webapp/packages/supersonic-fe/config/proxy.ts b/webapp/packages/supersonic-fe/config/proxy.ts index b494d09ce..2b6426e7a 100644 --- a/webapp/packages/supersonic-fe/config/proxy.ts +++ b/webapp/packages/supersonic-fe/config/proxy.ts @@ -1,7 +1,7 @@ export default { dev: { '/api/': { - target: 'http://10.91.217.39:9080', + target: 'http://127.0.0.1:9080', changeOrigin: true, }, }, diff --git a/webapp/packages/supersonic-fe/src/components/RightContent/AccessTokensModal.tsx b/webapp/packages/supersonic-fe/src/components/RightContent/AccessTokensModal.tsx new file mode 100644 index 000000000..d8c139286 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/components/RightContent/AccessTokensModal.tsx @@ -0,0 +1,195 @@ +import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; +import { Button, Form, Input, message, Modal, Table } from 'antd'; +import { useBoolean, useDynamicList, useRequest } from 'ahooks'; +import { + changePassword, + generateAccessToken, + getUserAccessTokens, + removeAccessToken, +} from '@/services/user'; +import { encryptPassword, encryptKey } from '@/utils/utils'; +import { API } from '@/services/API'; +import { EditableProTable, ProColumns } from '@ant-design/pro-components'; +import { CopyOutlined } from '@ant-design/icons'; + +type DataSourceType = { + id: React.Key; + name?: string; + token?: string; + expireDate?: string; + createDate?: string; + toBeSaved?: boolean; +}; + +export interface IRef { + open: () => void; + close: () => void; +} + +const ChangePasswordModal = forwardRef((_, ref) => { + const [open, { setTrue: openModal, setFalse: closeModal }] = useBoolean(false); + const [dataSource, setDataSource] = useState([]); + + const getAccessTokens = async () => { + try { + const res = await getUserAccessTokens(); + if (res && res.code === 200) { + return res.data; + } else { + message.error(res.msg); + return []; + } + } catch (error) { + message.error('获取数据失败,原因:' + error); + return []; + } + }; + + useImperativeHandle(ref, () => ({ + open: () => { + openModal(); + }, + close: () => { + closeModal(); + }, + })); + + const columns: ProColumns[] = [ + { + title: '名称', + dataIndex: 'name', + width: '15%', + formItemProps: { + rules: [{ required: true, message: '此项为必填项' }], + }, + editable: (text, record, index) => !!record.toBeSaved, + }, + { + title: '访问令牌', + dataIndex: 'token', + width: '25%', + formItemProps: (form, { rowIndex }) => { + return { + rules: [{ required: true, message: '此项为必填项' }], + }; + }, + render: (text, record, index) => { + // 脱敏处理, 点击图标可完整token + return record.toBeSaved ? ( + text + ) : ( + <> + {record.token ? record.token.slice(0, 5) + '********' + record.token.slice(-5) : ''} + + + ); + }, + editable: false, + }, + { + title: '过期时间', + dataIndex: 'expireDate', + valueType: 'date', + width: '20%', + formItemProps: { + rules: [{ required: true, message: '此项为必填项' }], + }, + editable: (text, record, index) => !!record.toBeSaved, + }, + { + title: '创建时间', + dataIndex: 'createDate', + valueType: 'date', + editable: false, + width: '20%', + }, + { + title: '操作', + valueType: 'option', + width: '15%', + render: (text, record, _, action) => [ + { + Modal.confirm({ + title: '删除访问令牌', + content: '确定删除此访问令牌吗?', + onOk: async () => { + const res = await removeAccessToken(record.id as number); + + if (res && res.code !== 200) { + message.error('删除失败,原因:' + res.msg); + return; + } + + setDataSource(dataSource.filter((item) => item.id !== record.id)); + message.success('删除成功'); + }, + }); + }} + > + 删除 + , + ], + }, + ]; + + return ( + + + rowKey="id" + recordCreatorProps={{ + position: 'bottom', + creatorButtonText: '新增访问令牌', + record: () => ({ id: (Math.random() * 1000000).toFixed(0), toBeSaved: true }), + }} + loading={false} + columns={columns} + request={async () => { + const data = await getAccessTokens(); + return { + data, + total: data.length, + success: true, + }; + }} + value={dataSource} + onChange={setDataSource} + editable={{ + type: 'single', + onSave: async (rowKey, data, row) => { + console.log(rowKey, data, row); + await generateAccessToken({ + name: data.name!, + expireTime: new Date(data.expireDate!).getTime() - new Date().getTime(), + }); + + const newTokens = await getAccessTokens(); + setTimeout(() => { + setDataSource(newTokens); + }, 100); + }, + }} + /> + + ); +}); + +export default ChangePasswordModal; diff --git a/webapp/packages/supersonic-fe/src/components/RightContent/AvatarDropdown.tsx b/webapp/packages/supersonic-fe/src/components/RightContent/AvatarDropdown.tsx index 289d37358..bf76a3d7a 100644 --- a/webapp/packages/supersonic-fe/src/components/RightContent/AvatarDropdown.tsx +++ b/webapp/packages/supersonic-fe/src/components/RightContent/AvatarDropdown.tsx @@ -1,10 +1,12 @@ -import React from 'react'; -import { LogoutOutlined } from '@ant-design/icons'; +import React, { useRef } from 'react'; +import { LogoutOutlined, KeyOutlined, UnlockOutlined } from '@ant-design/icons'; import { useModel } from 'umi'; import HeaderDropdown from '../HeaderDropdown'; import styles from './index.less'; import TMEAvatar from '../TMEAvatar'; import { AUTH_TOKEN_KEY } from '@/common/constants'; +import ChangePasswordModal, { IRef as IRefChangePasswordModal } from './ChangePasswordModal'; +import AccessTokensModal, { IRef as IAccessTokensModalRef } from './AccessTokensModal'; import { history } from 'umi'; export type GlobalHeaderRightProps = { @@ -27,7 +29,38 @@ const { APP_TARGET } = process.env; const AvatarDropdown: React.FC = () => { const { initialState = {}, setInitialState } = useModel('@@initialState'); const { currentUser = {} } = initialState as any; + const changePasswordModalRef = useRef(null); + const accessTokensModalRef = useRef(null); + + const handleAccessToken = () => { + accessTokensModalRef.current?.open(); + }; + + const handleChangePassword = () => { + changePasswordModalRef.current?.open(); + }; + const items = [ + { + label: ( + <> + + 访问令牌 + + ), + key: 'accessToken', + onClick: handleAccessToken, + }, + { + label: ( + <> + + 修改密码 + + ), + key: 'changePassword', + onClick: handleChangePassword, + }, { label: ( <> @@ -48,12 +81,21 @@ const AvatarDropdown: React.FC = () => { }, ]; return ( - - - - {currentUser.staffName} - - + <> + + + + {currentUser.staffName} + + + + + ); }; diff --git a/webapp/packages/supersonic-fe/src/components/RightContent/ChangePasswordModal.tsx b/webapp/packages/supersonic-fe/src/components/RightContent/ChangePasswordModal.tsx new file mode 100644 index 000000000..d0c9774ce --- /dev/null +++ b/webapp/packages/supersonic-fe/src/components/RightContent/ChangePasswordModal.tsx @@ -0,0 +1,121 @@ +import React, { forwardRef, useImperativeHandle } from 'react'; +import { Form, Input, message, Modal } from 'antd'; +import { useBoolean } from 'ahooks'; +import { changePassword } from '@/services/user'; +import { pick } from 'lodash'; +import { encryptPassword, encryptKey } from '@/utils/utils'; + +export interface IRef { + open: () => void; + close: () => void; +} + +const ChangePasswordModal = forwardRef((_, ref) => { + const [form] = Form.useForm(); + const [open, { setTrue: openModal, setFalse: closeModal }] = useBoolean(false); + const [confirmLoading, { set: setConfirmLoading }] = useBoolean(false); + + useImperativeHandle(ref, () => ({ + open: () => { + openModal(); + form.resetFields(); + }, + close: () => { + closeModal(); + form.resetFields(); + }, + })); + + const handleOk = async () => { + try { + const values = await form.validateFields(); + console.log(values); + setConfirmLoading(true); + // Call API to change password + const res = await changePassword({ + oldPassword: encryptPassword(values.oldPassword, encryptKey), + newPassword: encryptPassword(values.newPassword, encryptKey), + }); + + if (res && res.code !== 200) { + return message.warning(res.msg); + } + closeModal(); + } catch (error) { + console.log('Failed:', error); + } finally { + setConfirmLoading(false); + } + }; + + return ( + +
+ + + + + + + + + ({ + validator(_, value) { + if (!value || getFieldValue('newPassword') === value) { + return Promise.resolve(); + } + return Promise.reject(new Error('两次输入的密码不一致!')); + }, + }), + ]} + > + + +
+
+ ); +}); + +export default ChangePasswordModal; diff --git a/webapp/packages/supersonic-fe/src/services/API.d.ts b/webapp/packages/supersonic-fe/src/services/API.d.ts index 838714192..82cd823cd 100644 --- a/webapp/packages/supersonic-fe/src/services/API.d.ts +++ b/webapp/packages/supersonic-fe/src/services/API.d.ts @@ -23,6 +23,23 @@ declare namespace API { access?: 'user' | 'guest' | 'admin'; }; + export interface UserItem { + id: number; + name: string; + displayName: string; + email: string; + } + + export interface UserAccessToken { + createDate: string; + expireDate: string; + expireTime: number; + id: number; + name: string; + token: string; + userName: string; + } + export type LoginStateType = { status?: 'ok' | 'error'; type?: string; diff --git a/webapp/packages/supersonic-fe/src/services/user.ts b/webapp/packages/supersonic-fe/src/services/user.ts index 5516080b1..c6f1eeafe 100644 --- a/webapp/packages/supersonic-fe/src/services/user.ts +++ b/webapp/packages/supersonic-fe/src/services/user.ts @@ -20,3 +20,31 @@ export function saveSystemConfig(data: any): Promise { data, }); } + +export function changePassword(data: { newPassword: string; oldPassword: string }): Promise { + return request(`${process.env.AUTH_API_BASE_URL}user/resetPassword`, { + method: 'post', + data: { + newPassword: data.newPassword, + password: data.oldPassword, + }, + }); +} + +// 获取用户accessTokens +export async function getUserAccessTokens(): Promise> { + return request.get(`${process.env.AUTH_API_BASE_URL}user/getUserTokens`); +} + +export function generateAccessToken(data: { expireTime: number; name: string }): Promise { + return request(`${process.env.AUTH_API_BASE_URL}user/generateToken`, { + method: 'post', + data, + }); +} + +export function removeAccessToken(id: number): Promise { + return request(`${process.env.AUTH_API_BASE_URL}user/deleteUserToken?tokenId=${id}`, { + method: 'post', + }); +} diff --git a/webapp/packages/supersonic-fe/src/utils/utils.ts b/webapp/packages/supersonic-fe/src/utils/utils.ts index a36096371..fe546943d 100644 --- a/webapp/packages/supersonic-fe/src/utils/utils.ts +++ b/webapp/packages/supersonic-fe/src/utils/utils.ts @@ -472,7 +472,7 @@ export const objToArray = (_obj: ObjToArrayParams, keyType: string = 'string') = }); }; -const encryptKey = CryptoJS.enc.Hex.parse( +export const encryptKey = CryptoJS.enc.Hex.parse( '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', );