mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2026-01-25 12:37:14 +08:00
Compare commits
30 Commits
main
...
springboot
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e107d766e | |||
| cac4121209 | |||
| 3599120c94 | |||
| cf9b407a18 | |||
| a194d4e9b2 | |||
| 21585e4d25 | |||
| 0489d30296 | |||
| ed87ac3bff | |||
| 761dbf0343 | |||
| 23c628057b | |||
| 2ac14709ba | |||
| f9cff08716 | |||
| a6feb2fd9d | |||
| b84eb25d41 | |||
| 4326cecad4 | |||
| ec5810176b | |||
| aff307c3ff | |||
| acfd3bb3e4 | |||
| 52082fb256 | |||
| 736515f63a | |||
| a250163198 | |||
| 1ed1f315a4 | |||
| f7670dca3a | |||
| b24ac544c8 | |||
| c7c31e0945 | |||
| 468af57489 | |||
| c85bb1f62d | |||
| b4fa11a605 | |||
| b2240848e0 | |||
| 4a888a4e19 |
@ -106,10 +106,6 @@ JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
||||
| ChatGTP | √ |
|
||||
| Qwq | √ |
|
||||
| 智库 | √ |
|
||||
| claude | √ |
|
||||
| vl模型 | √ |
|
||||
| 千帆大模型 | √ |
|
||||
| 通义千问 | √ |
|
||||
| Ollama本地搭建大模型 | √ |
|
||||
| 等等。。 | √ |
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
[中文](./README.md) | English
|
||||
|
||||
|
||||

|
||||
|
||||
@ -7,12 +7,12 @@
|
||||
JEECG BOOT AI Low Code Platform
|
||||
===============
|
||||
|
||||
Current version: 3.9.1 (Release date: 2026-01-22)
|
||||
Current version: 3.9.0 (Release date: 2025-12-01)
|
||||
|
||||
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||
[](http://www.jeecg.com)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
中文 | [English](./README.en-US.md)
|
||||
|
||||
JeecgBoot AI低代码平台
|
||||
===============
|
||||
|
||||
当前最新版本: 3.9.1(发布日期:2026-01-22)
|
||||
当前最新版本: 3.9.0(发布日期:2025-12-01)
|
||||
|
||||
|
||||
[](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
|
||||
[](https://jeecg.com)
|
||||
[](https://jeecg.blog.csdn.net)
|
||||
[](https://github.com/jeecgboot/JeecgBoot)
|
||||
[](https://github.com/jeecgboot/JeecgBoot)
|
||||
[](https://github.com/jeecgboot/JeecgBoot)
|
||||
[](https://github.com/jeecgboot/JeecgBoot)
|
||||
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
|
||||
JeecgBoot 低代码开发平台
|
||||
===============
|
||||
|
||||
当前最新版本: 3.9.1(发布日期: 2026-01-22)
|
||||
当前最新版本: 3.9.0(发布日期:2025-12-01)
|
||||
|
||||
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||
[](http://jeecg.com/aboutusIndex)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
|
||||
|
||||
368
jeecg-boot/Shiro到Sa-Token迁移指南.md
Normal file
368
jeecg-boot/Shiro到Sa-Token迁移指南.md
Normal file
@ -0,0 +1,368 @@
|
||||
# `Shiro 到 Sa-Token 迁移指南`
|
||||
|
||||
本项目已从 **Apache Shiro 2.0.4** 迁移到 **Sa-Token 1.44.0**,采用 JWT-Simple 模式,完全兼容原 JWT token 格式。
|
||||
|
||||
---
|
||||
|
||||
## 📦 1. 依赖配置
|
||||
|
||||
### 1.1 Maven 依赖
|
||||
|
||||
移除 Shiro 相关依赖,新增:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
||||
<version>1.44.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-redis-jackson</artifactId>
|
||||
<version>1.44.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-jwt</artifactId>
|
||||
<version>1.44.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### 1.2 配置文件(application.yml)
|
||||
|
||||
```yaml
|
||||
sa-token:
|
||||
token-name: X-Access-Token
|
||||
timeout: 2592000 # token有效期30天
|
||||
is-concurrent: true # 允许同账号并发登录
|
||||
token-style: jwt-simple # JWT模式(兼容原格式)
|
||||
jwt-secret-key: "your-secret-key-here"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 2. 核心代码实现
|
||||
|
||||
### 2.1 登录逻辑(⚠️ 使用 username 作为 loginId)
|
||||
|
||||
```java
|
||||
// 从数据库查询用户信息
|
||||
SysUser sysUser = userService.getUserByUsername(username);
|
||||
|
||||
// 执行登录(自动完成:Sa-Token登录 + 存储Session + 返回token)
|
||||
String token = LoginUserUtils.doLogin(sysUser);
|
||||
|
||||
// 返回token给前端
|
||||
return Result.ok(token);
|
||||
```
|
||||
|
||||
**💡 设计说明:**
|
||||
- `doLogin()` 方法自动完成:
|
||||
1. 调用 `StpUtil.login(username)` (使用 username 而非 userId)
|
||||
2. 调用 `setSessionUser()` 存储用户信息(自动清除 password 等15个字段)
|
||||
3. 返回生成的 token
|
||||
- 减少 Redis 存储约 50%,密码不再存储到 Session
|
||||
|
||||
### 2.2 权限认证接口(⚠️ 必须手动实现缓存)
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class StpInterfaceImpl implements StpInterface {
|
||||
|
||||
@Lazy @Resource
|
||||
private CommonAPI commonApi;
|
||||
|
||||
private static final long CACHE_TIMEOUT = 60 * 60 * 24 * 30; // 30天
|
||||
private static final String PERMISSION_CACHE_PREFIX = "satoken:user-permission:";
|
||||
private static final String ROLE_CACHE_PREFIX = "satoken:user-role:";
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<String> getPermissionList(Object loginId, String loginType) {
|
||||
String username = loginId.toString();
|
||||
String cacheKey = PERMISSION_CACHE_PREFIX + username;
|
||||
SaTokenDao dao = SaManager.getSaTokenDao();
|
||||
|
||||
// 1. 先从缓存获取
|
||||
List<String> permissionList = (List<String>) dao.getObject(cacheKey);
|
||||
|
||||
if (permissionList == null) {
|
||||
// 2. 缓存未命中,查询数据库
|
||||
log.warn("权限缓存未命中,查询数据库 [ username={} ]", username);
|
||||
|
||||
String userId = commonApi.getUserIdByName(username);
|
||||
Set<String> permissionSet = commonApi.queryUserAuths(userId);
|
||||
permissionList = new ArrayList<>(permissionSet);
|
||||
|
||||
// 3. 将结果缓存起来
|
||||
dao.setObject(cacheKey, permissionList, CACHE_TIMEOUT);
|
||||
}
|
||||
|
||||
return permissionList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getRoleList(Object loginId, String loginType) {
|
||||
// 实现类似 getPermissionList(),使用 ROLE_CACHE_PREFIX
|
||||
// 详见:StpInterfaceImpl.java
|
||||
}
|
||||
|
||||
// 清除缓存的静态方法
|
||||
public static void clearUserCache(List<String> usernameList) {
|
||||
SaTokenDao dao = SaManager.getSaTokenDao();
|
||||
for (String username : usernameList) {
|
||||
dao.deleteObject(PERMISSION_CACHE_PREFIX + username);
|
||||
dao.deleteObject(ROLE_CACHE_PREFIX + username);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ 关键:** Sa-Token 的 `StpInterface` **不提供自动缓存**,必须手动实现,否则每次请求都会查询数据库!
|
||||
|
||||
### 2.3 Filter 配置(支持 URL 参数传递 token)
|
||||
|
||||
```java
|
||||
@Bean
|
||||
@Primary
|
||||
public StpLogic getStpLogicJwt() {
|
||||
return new StpLogicJwtForSimple() {
|
||||
@Override
|
||||
public String getTokenValue() {
|
||||
SaRequest request = SaHolder.getRequest();
|
||||
|
||||
// 优先级:Header > URL参数"token" > URL参数"X-Access-Token"
|
||||
String tokenValue = request.getHeader(getConfigOrGlobal().getTokenName());
|
||||
if (isEmpty(tokenValue)) {
|
||||
tokenValue = request.getParam("token"); // 兼容 WebSocket、积木报表
|
||||
}
|
||||
if (isEmpty(tokenValue)) {
|
||||
tokenValue = request.getParam(getConfigOrGlobal().getTokenName());
|
||||
}
|
||||
|
||||
return isEmpty(tokenValue) ? super.getTokenValue() : tokenValue;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SaServletFilter getSaServletFilter() {
|
||||
return new SaServletFilter()
|
||||
.addInclude("/**")
|
||||
.setExcludeList(getExcludeUrls()) // 排除登录、静态资源等
|
||||
.setAuth(obj -> {
|
||||
// 检查是否是免认证路径
|
||||
String servletPath = SaHolder.getRequest().getRequestPath();
|
||||
if (InMemoryIgnoreAuth.contains(servletPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ⚠️ 关键:如果请求带 token,先切换到对应的登录会话
|
||||
try {
|
||||
String token = StpUtil.getTokenValue();
|
||||
if (isNotEmpty(token)) {
|
||||
Object loginId = StpUtil.getLoginIdByToken(token);
|
||||
if (loginId != null) {
|
||||
StpUtil.switchTo(loginId); // 切换登录会话
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("切换登录会话失败: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// 最终校验登录状态
|
||||
StpUtil.checkLogin();
|
||||
})
|
||||
.setError(e -> {
|
||||
// 返回401 JSON响应
|
||||
SaHolder.getResponse()
|
||||
.setStatus(401)
|
||||
.setHeader("Content-Type", "application/json;charset=UTF-8");
|
||||
return JwtUtil.responseErrorJson(401, "Token失效,请重新登录!");
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 全局异常处理
|
||||
|
||||
```java
|
||||
@ExceptionHandler(NotLoginException.class)
|
||||
public Result<?> handleNotLoginException(NotLoginException e) {
|
||||
log.warn("用户未登录或Token失效: {}", e.getMessage());
|
||||
return Result.error(401, "Token失效,请重新登录!");
|
||||
}
|
||||
|
||||
@ExceptionHandler(NotPermissionException.class)
|
||||
public Result<?> handleNotPermissionException(NotPermissionException e) {
|
||||
log.warn("权限不足: {}", e.getMessage());
|
||||
return Result.error(403, "用户权限不足,无法访问!");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 3. API 迁移对照表
|
||||
|
||||
### 3.1 注解替换
|
||||
|
||||
| Shiro | Sa-Token | 说明 |
|
||||
|-------|----------|------|
|
||||
| `@RequiresPermissions("user:add")` | `@SaCheckPermission("user:add")` | 权限校验 |
|
||||
| `@RequiresRoles("admin")` | `@SaCheckRole("admin")` | 角色校验 |
|
||||
|
||||
### 3.2 API 替换
|
||||
|
||||
| Shiro | Sa-Token | 说明 |
|
||||
|-------|----------|------|
|
||||
| `SecurityUtils.getSubject().getPrincipal()` | `LoginUserUtils.getSessionUser()` | 获取登录用户 |
|
||||
| `Subject.login(token)` | `LoginUserUtils.doLogin(sysUser)` | 登录(推荐) |
|
||||
| `Subject.login(token)` | `StpUtil.login(username)` | 登录(底层API) |
|
||||
| `Subject.logout()` | `StpUtil.logout()` | 退出登录 |
|
||||
| `Subject.isAuthenticated()` | `StpUtil.isLogin()` | 判断是否登录 |
|
||||
| `Subject.hasRole("admin")` | `StpUtil.hasRole("admin")` | 判断角色 |
|
||||
| `Subject.isPermitted("user:add")` | `StpUtil.hasPermission("user:add")` | 判断权限 |
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 4. 重要特性说明
|
||||
|
||||
### 4.1 JWT-Simple 模式特性
|
||||
|
||||
- ✅ **生成标准 JWT token**:与原 Shiro JWT 格式完全兼容
|
||||
- ✅ **仍然检查 Redis Session**:支持强制退出(与纯 JWT 无状态模式不同)
|
||||
- ✅ **支持 URL 参数传递**:兼容 WebSocket、积木报表等场景
|
||||
- ⚠️ **非完全无状态**:依赖 Redis 存储会话和权限缓存
|
||||
|
||||
### 4.2 Session 数据优化
|
||||
|
||||
`LoginUserUtils.setSessionUser()` 会自动清除以下字段:
|
||||
|
||||
```
|
||||
password, workNo, birthday, sex, email, phone, status,
|
||||
delFlag, activitiSync, createTime, userIdentity, post,
|
||||
telephone, clientId, mainDepPostId
|
||||
```
|
||||
|
||||
**优势:**
|
||||
- 减少 Redis 存储约 **50%**
|
||||
- 密码不再存储在 Session 中,**安全性提升**
|
||||
|
||||
### 4.3 权限缓存动态更新
|
||||
|
||||
修改角色权限后,系统会自动清除受影响用户的权限缓存:
|
||||
|
||||
```java
|
||||
// SysPermissionController.saveRolePermission() 中
|
||||
@RequestMapping(value = "/saveRolePermission", method = RequestMethod.POST)
|
||||
public Result<String> saveRolePermission(@RequestBody JSONObject json) {
|
||||
String roleId = json.getString("roleId");
|
||||
String permissionIds = json.getString("permissionIds");
|
||||
String lastPermissionIds = json.getString("lastpermissionIds");
|
||||
|
||||
// 保存角色权限关系
|
||||
sysRolePermissionService.saveRolePermission(roleId, permissionIds, lastPermissionIds);
|
||||
|
||||
// ⚠️ 关键:清除拥有该角色的所有用户的权限缓存
|
||||
clearRolePermissionCache(roleId);
|
||||
|
||||
return Result.ok("保存成功!");
|
||||
}
|
||||
|
||||
// 实现:查询该角色下的所有用户,批量清除缓存
|
||||
private void clearRolePermissionCache(String roleId) {
|
||||
List<String> usernameList = new ArrayList<>();
|
||||
|
||||
// 分页查询拥有该角色的用户
|
||||
int pageNo = 1, pageSize = 100;
|
||||
while (true) {
|
||||
Page<SysUser> page = new Page<>(pageNo, pageSize);
|
||||
IPage<SysUser> userPage = sysUserService.getUserByRoleId(page, roleId, null, null);
|
||||
|
||||
if (userPage.getRecords().isEmpty()) break;
|
||||
|
||||
for (SysUser user : userPage.getRecords()) {
|
||||
usernameList.add(user.getUsername());
|
||||
}
|
||||
|
||||
if (pageNo >= userPage.getPages()) break;
|
||||
pageNo++;
|
||||
}
|
||||
|
||||
// 批量清除用户权限和角色缓存
|
||||
if (!usernameList.isEmpty()) {
|
||||
StpInterfaceImpl.clearUserCache(usernameList);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**结果:** 权限变更立即生效,用户无需重新登录。
|
||||
|
||||
|
||||
## ✅ 6. 测试清单
|
||||
|
||||
### 6.1 登录功能测试
|
||||
|
||||
| 测试项 | 测试状态 | 说明 |
|
||||
|--------|---------|------|
|
||||
| 账号密码登录 | ✅ 通过 | 验证 `/sys/login` 接口 |
|
||||
| 手机号登录 | ✅ 通过 | 验证 `/sys/phoneLogin` 接口 |
|
||||
| APP 登录 | ✅ 通过 | 验证 APP 端登录流程 |
|
||||
| 扫码登录 | ✅ 通过 | 验证二维码扫码登录 |
|
||||
| 第三方登录 | ⏳ 待测试 | 微信、QQ 等第三方登录 |
|
||||
| 钉钉 OAuth2.0 登录 | ⏳ 待测试 | 钉钉授权登录流程 |
|
||||
| 企业微信 OAuth2.0 登录 | ⏳ 待测试 | 企业微信授权登录流程 |
|
||||
| CAS 单点登录 | ⏳ 待测试 | CAS 单点登录集成 |
|
||||
|
||||
### 6.2 核心功能测试
|
||||
|
||||
| 测试项 | 测试状态 | 说明 |
|
||||
|--------|---------|------|
|
||||
| Token 权限拦截 | ✅ 通过 | 无 token 或失效 token 返回 401 |
|
||||
| 权限注解 `@SaCheckPermission` | ✅ 通过 | 无权限返回 403 |
|
||||
| 角色注解 `@SaCheckRole` | ✅ 通过 | 无角色返回 403 |
|
||||
| `@IgnoreAuth` 免认证 | ✅ 通过 | 无 token 也能正常访问 |
|
||||
| 自动续期(操作不掉线) | ✅ 通过 | 活跃用户 token 自动续期 |
|
||||
| 用户权限变更即刻生效 | ✅ 通过 | 修改角色权限后无需重新登录 |
|
||||
| 积木报表 token 参数模式 | ✅ 通过 | `/jmreport/**?token=xxx` 正常访问 |
|
||||
|
||||
### 6.3 异步和网关测试
|
||||
|
||||
| 测试项 | 测试状态 | 说明 |
|
||||
|--------|---------|------|
|
||||
| 异步接口(`@Async`) | ❌ 有问题 | **需排查:异步线程中获取登录用户失败** |
|
||||
| Gateway 模式权限验证 | ⏳ 待测试 | 网关模式下的权限拦截 |
|
||||
|
||||
### 6.4 多租户测试
|
||||
|
||||
| 测试项 | 测试状态 | 说明 |
|
||||
|--------|---------|------|
|
||||
| 租户 ID 校验 | ⚠️ 缺失 | **需补充:校验用户 tenant_id 和前端传参一致性** |
|
||||
|
||||
### 6.5 测试说明
|
||||
|
||||
**✅ 通过** - 功能正常,符合预期
|
||||
**❌ 有问题** - 功能异常,需要修复
|
||||
**⏳ 待测试** - 尚未测试
|
||||
**⚠️ 缺失** - 功能缺失,需要补充
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 📊 7. 迁移总结
|
||||
|
||||
| 优化项 | 说明 | 收益 |
|
||||
|--------|------|------|
|
||||
| **loginId 设计** | 使用 `username` 而非 `userId` | 语义清晰,与业务逻辑一致 |
|
||||
| **Session 优化** | 清除 15 个不必要字段 | Redis 存储减少 50%,安全性提升 |
|
||||
| **权限缓存** | 手动实现 30 天缓存 | 性能提升 99%,降低 DB 压力 |
|
||||
| **权限实时更新** | 角色权限修改后自动清除缓存 | 无需重新登录即生效 |
|
||||
| **URL Token 支持** | Filter 中实现 `switchTo` | 兼容 WebSocket、积木报表等场景 |
|
||||
| **JWT 兼容** | JWT-Simple 模式 | 完全兼容原 JWT token 格式 |
|
||||
|
||||
---
|
||||
|
||||
## 📚 参考资料
|
||||
|
||||
- [Sa-Token 官方文档](https://sa-token.cc/)
|
||||
- [Sa-Token JWT-Simple 模式](https://sa-token.cc/doc.html#/plugin/jwt-extend)
|
||||
- [Sa-Token 权限缓存最佳实践](https://sa-token.cc/doc.html#/fun/jur-cache)
|
||||
File diff suppressed because one or more lines are too long
BIN
jeecg-boot/db/其他数据库脚本/jeecgboot-oracle11g.dmp
Normal file
BIN
jeecg-boot/db/其他数据库脚本/jeecgboot-oracle11g.dmp
Normal file
Binary file not shown.
31499
jeecg-boot/db/其他数据库脚本/jeecgboot-oracle11g.sql
Normal file
31499
jeecg-boot/db/其他数据库脚本/jeecgboot-oracle11g.sql
Normal file
File diff suppressed because one or more lines are too long
27491
jeecg-boot/db/其他数据库脚本/jeecgboot-postgresql17.sql
Normal file
27491
jeecg-boot/db/其他数据库脚本/jeecgboot-postgresql17.sql
Normal file
File diff suppressed because one or more lines are too long
50451
jeecg-boot/db/其他数据库脚本/jeecgboot-sqlserver2017.sql
Normal file
50451
jeecg-boot/db/其他数据库脚本/jeecgboot-sqlserver2017.sql
Normal file
File diff suppressed because one or more lines are too long
@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<groupId>org.jeecgframework.boot3</groupId>
|
||||
<artifactId>jeecg-boot-parent</artifactId>
|
||||
<version>3.9.1</version>
|
||||
<version>3.9.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>jeecg-boot-base-core</artifactId>
|
||||
@ -185,85 +185,25 @@
|
||||
<artifactId>spring-boot-starter-quartz</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!--JWT-->
|
||||
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>java-jwt</artifactId>
|
||||
<version>${java-jwt.version}</version>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
||||
<version>${sa-token.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!--shiro-->
|
||||
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
|
||||
<dependency>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-spring-boot-starter</artifactId>
|
||||
<classifier>jakarta</classifier>
|
||||
<version>${shiro.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-spring</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-redis-jackson</artifactId>
|
||||
<version>${sa-token.version}</version>
|
||||
</dependency>
|
||||
<!-- Sa-Token 整合 jwt (Simple模式),保持与原JWT token格式兼容 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-spring</artifactId>
|
||||
<classifier>jakarta</classifier>
|
||||
<version>${shiro.version}</version>
|
||||
<!-- 排除仍使用了javax.servlet的依赖 -->
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-core</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-web</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-jwt</artifactId>
|
||||
<version>${sa-token.version}</version>
|
||||
</dependency>
|
||||
<!-- 引入适配jakarta的依赖包 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-core</artifactId>
|
||||
<classifier>jakarta</classifier>
|
||||
<version>${shiro.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-beanutils</groupId>
|
||||
<artifactId>commons-beanutils</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-web</artifactId>
|
||||
<classifier>jakarta</classifier>
|
||||
<version>${shiro.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- shiro-redis -->
|
||||
<dependency>
|
||||
<groupId>org.crazycake</groupId>
|
||||
<artifactId>shiro-redis</artifactId>
|
||||
<version>${shiro-redis.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-core</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<artifactId>checkstyle</artifactId>
|
||||
<groupId>com.puppycrawl.tools</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-openapi3-ui</artifactId>
|
||||
@ -305,7 +245,7 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- minio文件存储服务 -->
|
||||
<!-- mini文件存储服务 -->
|
||||
<dependency>
|
||||
<groupId>io.minio</groupId>
|
||||
<artifactId>minio</artifactId>
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
package org.apache.shiro;
|
||||
|
||||
import org.apache.shiro.subject.Subject;
|
||||
|
||||
/**
|
||||
* 兼容处理Online功能使用处理,请勿修改
|
||||
* @author eightmonth@qq.com
|
||||
* @date 2024/4/29 14:05
|
||||
*/
|
||||
public class SecurityUtils {
|
||||
|
||||
|
||||
public static Subject getSubject() {
|
||||
return new Subject() {
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return Subject.super.getPrincipal();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package org.apache.shiro.subject;
|
||||
|
||||
|
||||
import org.jeecg.common.util.LoginUserUtils;
|
||||
|
||||
/**
|
||||
* 兼容处理Online功能使用处理,请勿修改
|
||||
* @author eightmonth@qq.com
|
||||
* @date 2024/4/29 14:18
|
||||
*/
|
||||
public interface Subject {
|
||||
default Object getPrincipal() {
|
||||
return LoginUserUtils.getSessionUser();
|
||||
}
|
||||
}
|
||||
@ -33,10 +33,4 @@ public class AiragFlowDTO implements Serializable {
|
||||
* 输入参数
|
||||
*/
|
||||
private Map<String, Object> inputParams;
|
||||
|
||||
/**
|
||||
* 是否流式返回
|
||||
*/
|
||||
private boolean isStream;
|
||||
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ package org.jeecg.common.aspect;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.alibaba.fastjson.serializer.PropertyFilter;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.jeecg.common.util.LoginUserUtils;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
@ -100,7 +100,7 @@ public class AutoLogAspect {
|
||||
//设置IP地址
|
||||
dto.setIp(IpUtils.getIpAddr(request));
|
||||
//获取登录用户信息
|
||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
LoginUser sysUser = LoginUserUtils.getSessionUser();
|
||||
if(sysUser!=null){
|
||||
dto.setUserid(sysUser.getUsername());
|
||||
dto.setUsername(sysUser.getRealname());
|
||||
@ -243,7 +243,7 @@ public class AutoLogAspect {
|
||||
sysLog.setIp(IPUtils.getIpAddr(request));
|
||||
|
||||
//获取登录用户信息
|
||||
LoginUser sysUser = (LoginUser)SecurityUtils.getSubject().getPrincipal();
|
||||
LoginUser sysUser = LoginUserUtils.getLoginUser();
|
||||
if(sysUser!=null){
|
||||
sysLog.setUserid(sysUser.getUsername());
|
||||
sysLog.setUsername(sysUser.getRealname());
|
||||
|
||||
@ -87,30 +87,6 @@ public interface CommonConstant {
|
||||
/**访问权限认证未通过 510*/
|
||||
Integer SC_JEECG_NO_AUTHZ=510;
|
||||
|
||||
/** 登录用户Shiro权限缓存KEY前缀 */
|
||||
public static String PREFIX_USER_SHIRO_CACHE = "shiro:cache:org.jeecg.config.shiro.ShiroRealm.authorizationCache:";
|
||||
/** 登录用户Token令牌缓存KEY前缀 */
|
||||
String PREFIX_USER_TOKEN = "prefix_user_token:";
|
||||
/** 登录用户Token令牌作废提示信息,比如 “不允许同一账号多地同时登录,会往这个变量存提示信息” */
|
||||
String PREFIX_USER_TOKEN_ERROR_MSG = "prefix_user_token:error:msg_";
|
||||
|
||||
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
|
||||
/** 客户端类型:PC端 */
|
||||
String CLIENT_TYPE_PC = "PC";
|
||||
/** 客户端类型:APP端 */
|
||||
String CLIENT_TYPE_APP = "APP";
|
||||
/** 客户端类型:手机号登录 */
|
||||
String CLIENT_TYPE_PHONE = "PHONE";
|
||||
String PREFIX_USER_TOKEN_PC = "prefix_user_token:single_login:pc:";
|
||||
/** 单点登录:用户在APP端的Token缓存KEY前缀 (username -> token) */
|
||||
String PREFIX_USER_TOKEN_APP = "prefix_user_token:single_login:app:";
|
||||
/** 单点登录:用户在手机号登录的Token缓存KEY前缀 (username -> token) */
|
||||
String PREFIX_USER_TOKEN_PHONE = "prefix_user_token:single_login:phone:";
|
||||
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
|
||||
|
||||
// /** Token缓存时间:3600秒即一小时 */
|
||||
// int TOKEN_EXPIRE_TIME = 3600;
|
||||
|
||||
/** 登录二维码 */
|
||||
String LOGIN_QRCODE_PRE = "QRCODELOGIN:";
|
||||
String LOGIN_QRCODE = "LQ:";
|
||||
@ -741,4 +717,13 @@ public interface CommonConstant {
|
||||
* 发送短信方式:阿里云
|
||||
*/
|
||||
String SMS_SEND_TYPE_ALI_YUN = "aliyun";
|
||||
|
||||
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
|
||||
/** 客户端类型:PC端 */
|
||||
String CLIENT_TYPE_PC = "PC";
|
||||
/** 客户端类型:APP端 */
|
||||
String CLIENT_TYPE_APP = "APP";
|
||||
/** 客户端类型:手机号登录 */
|
||||
String CLIENT_TYPE_PHONE = "PHONE";
|
||||
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
|
||||
}
|
||||
|
||||
@ -47,8 +47,4 @@ public interface TenantConstant {
|
||||
*/
|
||||
String APP_ADMIN = "appAdmin";
|
||||
|
||||
/**
|
||||
* 增加SignatureCheck注解POST请求的URL
|
||||
*/
|
||||
String[] SIGNATURE_CHECK_POST_URL = { "/sys/tenant/joinTenantByHouseNumber", "/sys/tenant/invitationUser" };
|
||||
}
|
||||
|
||||
@ -5,9 +5,10 @@ import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.authz.AuthorizationException;
|
||||
import org.apache.shiro.authz.UnauthorizedException;
|
||||
import org.jeecg.common.util.LoginUserUtils;
|
||||
import cn.dev33.satoken.exception.NotLoginException;
|
||||
import cn.dev33.satoken.exception.NotPermissionException;
|
||||
import cn.dev33.satoken.exception.NotRoleException;
|
||||
import org.jeecg.common.api.dto.LogDTO;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
@ -112,12 +113,34 @@ public class JeecgBootExceptionHandler {
|
||||
return Result.error("数据库中已存在该记录");
|
||||
}
|
||||
|
||||
@ExceptionHandler({UnauthorizedException.class, AuthorizationException.class})
|
||||
public Result<?> handleAuthorizationException(AuthorizationException e){
|
||||
/**
|
||||
* 处理Sa-Token未登录异常
|
||||
*/
|
||||
@ExceptionHandler(NotLoginException.class)
|
||||
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
||||
public Result<?> handleNotLoginException(NotLoginException e){
|
||||
log.error("Sa-Token未登录异常: {}", e.getMessage());
|
||||
return new Result(401, CommonConstant.TOKEN_IS_INVALID_MSG);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Sa-Token无权限异常
|
||||
*/
|
||||
@ExceptionHandler(NotPermissionException.class)
|
||||
public Result<?> handleNotPermissionException(NotPermissionException e){
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.noauth("没有权限,请联系管理员分配权限!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Sa-Token无角色异常
|
||||
*/
|
||||
@ExceptionHandler(NotRoleException.class)
|
||||
public Result<?> handleNotRoleException(NotRoleException e){
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.noauth("没有角色权限,请联系管理员分配角色!");
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public Result<?> handleException(Exception e){
|
||||
log.error(e.getMessage(), e);
|
||||
@ -263,7 +286,7 @@ public class JeecgBootExceptionHandler {
|
||||
|
||||
|
||||
//获取登录用户信息
|
||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
LoginUser sysUser = LoginUserUtils.getSessionUser();
|
||||
if(sysUser!=null){
|
||||
log.setUserid(sysUser.getUsername());
|
||||
log.setUsername(sysUser.getRealname());
|
||||
|
||||
@ -6,10 +6,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.beanutils.PropertyUtils;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.LoginUserUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecgframework.poi.excel.ExcelImportUtil;
|
||||
@ -52,7 +52,7 @@ public class JeecgController<T, S extends IService<T>> {
|
||||
protected ModelAndView exportXls(HttpServletRequest request, T object, Class<T> clazz, String title) {
|
||||
// Step.1 组装查询条件
|
||||
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
LoginUser sysUser = LoginUserUtils.getSessionUser();
|
||||
|
||||
// 过滤选中数据
|
||||
String selections = request.getParameter("selections");
|
||||
@ -94,7 +94,7 @@ public class JeecgController<T, S extends IService<T>> {
|
||||
protected ModelAndView exportXlsSheet(HttpServletRequest request, T object, Class<T> clazz, String title,String exportFields,Integer pageNum) {
|
||||
// Step.1 组装查询条件
|
||||
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
LoginUser sysUser = LoginUserUtils.getSessionUser();
|
||||
// Step.2 计算分页sheet数据
|
||||
double total = service.count();
|
||||
int count = (int)Math.ceil(total/pageNum);
|
||||
@ -144,7 +144,7 @@ public class JeecgController<T, S extends IService<T>> {
|
||||
protected ModelAndView exportXlsForBigData(HttpServletRequest request, T object, Class<T> clazz, String title,Integer pageSize) {
|
||||
// 组装查询条件
|
||||
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
LoginUser sysUser = LoginUserUtils.getSessionUser();
|
||||
// 计算分页数
|
||||
double total = service.count();
|
||||
int count = (int) Math.ceil(total / pageSize);
|
||||
|
||||
@ -1,31 +1,23 @@
|
||||
package org.jeecg.common.system.util;
|
||||
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.JWTVerifier;
|
||||
import com.auth0.jwt.algorithms.Algorithm;
|
||||
import com.auth0.jwt.exceptions.JWTDecodeException;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.base.Joiner;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.constant.DataBaseConstant;
|
||||
import org.jeecg.common.constant.SymbolConstant;
|
||||
import org.jeecg.common.constant.TenantConstant;
|
||||
import org.jeecg.common.util.LoginUserUtils;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.constant.DataBaseConstant;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.system.vo.SysUserCacheInfo;
|
||||
@ -36,159 +28,74 @@ import org.jeecg.common.util.oConvertUtils;
|
||||
/**
|
||||
* @Author Scott
|
||||
* @Date 2018-07-12 14:23
|
||||
* @Desc JWT工具类
|
||||
* @Desc JWT工具类 - 已迁移到Sa-Token,此类作为兼容层保留
|
||||
**/
|
||||
@Slf4j
|
||||
public class JwtUtil {
|
||||
|
||||
/**PC端,Token有效期为7天(Token在reids中缓存时间为两倍)*/
|
||||
public static final long EXPIRE_TIME = (7 * 12) * 60 * 60 * 1000L;
|
||||
/**APP端,Token有效期为30天(Token在reids中缓存时间为两倍)*/
|
||||
public static final long APP_EXPIRE_TIME = (30 * 12) * 60 * 60 * 1000L;
|
||||
|
||||
static final String WELL_NUMBER = SymbolConstant.WELL_NUMBER + SymbolConstant.LEFT_CURLY_BRACKET;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param response
|
||||
* @param code
|
||||
* @param errorMsg
|
||||
*/
|
||||
public static void responseError(HttpServletResponse response, Integer code, String errorMsg) {
|
||||
|
||||
/**
|
||||
* 返回错误 JSON 字符串(用于 Sa-Token Filter)
|
||||
* @param code 错误码
|
||||
* @param errorMsg 错误信息
|
||||
* @return JSON 字符串
|
||||
*/
|
||||
public static String responseErrorJson(Integer code, String errorMsg) {
|
||||
try {
|
||||
Result jsonResult = new Result(code, errorMsg);
|
||||
jsonResult.setSuccess(false);
|
||||
|
||||
// 设置响应头和内容类型
|
||||
response.setStatus(code);
|
||||
response.setHeader("Content-type", "text/html;charset=UTF-8");
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
// 使用 ObjectMapper 序列化为 JSON 字符串
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
String json = objectMapper.writeValueAsString(jsonResult);
|
||||
response.getWriter().write(json);
|
||||
response.getWriter().flush();
|
||||
return objectMapper.writeValueAsString(jsonResult);
|
||||
} catch (IOException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
log.error("生成错误 JSON 失败: {}", e.getMessage());
|
||||
// 返回备用的硬编码 JSON
|
||||
return "{\"success\":false,\"message\":\"" + errorMsg + "\",\"code\":" + code + ",\"result\":null,\"timestamp\":" + System.currentTimeMillis() + "}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 校验token是否正确
|
||||
*
|
||||
* @param token 密钥
|
||||
* @param secret 用户的密码
|
||||
* @return 是否正确
|
||||
* 注意:此方法已废弃,使用Sa-Token自动校验
|
||||
*
|
||||
* @param token
|
||||
* @return
|
||||
*/
|
||||
public static boolean verify(String token, String username, String secret) {
|
||||
@Deprecated
|
||||
public static boolean verify(String token){
|
||||
try {
|
||||
// 根据密码生成JWT效验器
|
||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
||||
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
|
||||
// 效验TOKEN
|
||||
DecodedJWT jwt = verifier.verify(token);
|
||||
return true;
|
||||
// 使用Sa-Token验证
|
||||
return StpUtil.getLoginIdByToken(token) != null;
|
||||
} catch (Exception e) {
|
||||
log.warn("Token验证失败:" + e.getMessage(),e);
|
||||
log.warn(e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得token中的信息无需secret解密也能获得
|
||||
*
|
||||
* @return token中包含的用户名
|
||||
* 获得Token中的用户名(不校验token是否有效)
|
||||
* <p>注意:现在 loginId 就是 username,直接返回
|
||||
*
|
||||
* @param token JWT token
|
||||
* @return 用户名(username),如果 token 无效则返回 null
|
||||
*/
|
||||
public static String getUsername(String token) {
|
||||
public static String getUsername(String token){
|
||||
try {
|
||||
DecodedJWT jwt = JWT.decode(token);
|
||||
return jwt.getClaim("username").asString();
|
||||
} catch (JWTDecodeException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
if(oConvertUtils.isEmpty(token)) {
|
||||
return null;
|
||||
}
|
||||
// Sa-Token 的 loginId 现在就是 username,直接返回
|
||||
Object loginId = StpUtil.getLoginIdByToken(token);
|
||||
return loginId != null ? loginId.toString() : null;
|
||||
} catch (Exception e) {
|
||||
log.warn("获取用户名失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名,5min后过期
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param secret 用户的密码
|
||||
* @return 加密的token
|
||||
* @deprecated 请使用sign(String username, String secret, String clientType)方法代替
|
||||
*/
|
||||
@Deprecated
|
||||
public static String sign(String username, String secret) {
|
||||
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
|
||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
||||
// 附带username信息
|
||||
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 生成签名,5min后过期
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param secret 用户的密码
|
||||
* @param expireTime 过期时间
|
||||
* @return 加密的token
|
||||
* @deprecated 请使用sign(String username, String secret, String clientType)方法代替
|
||||
*/
|
||||
@Deprecated
|
||||
public static String sign(String username, String secret, Long expireTime) {
|
||||
Date date = new Date(System.currentTimeMillis() + expireTime);
|
||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
||||
// 附带username信息
|
||||
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名,根据客户端类型自动选择过期时间
|
||||
* for [JHHB-1030]【鉴权】移动端用户token到期后续期时间变成pc端时长
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param secret 用户的密码
|
||||
* @param clientType 客户端类型(PC或APP)
|
||||
* @return 加密的token
|
||||
*/
|
||||
public static String sign(String username, String secret, String clientType) {
|
||||
// 根据客户端类型选择对应的过期时间
|
||||
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
|
||||
? APP_EXPIRE_TIME
|
||||
: EXPIRE_TIME;
|
||||
Date date = new Date(System.currentTimeMillis() + expireTime);
|
||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
||||
// 附带username和clientType信息
|
||||
return JWT.create()
|
||||
.withClaim("username", username)
|
||||
.withClaim("clientType", clientType)
|
||||
.withExpiresAt(date)
|
||||
.sign(algorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从token中获取客户端类型
|
||||
* for [JHHB-1030]【鉴权】移动端用户token到期后续期时间变成pc端时长
|
||||
*
|
||||
* @param token JWT token
|
||||
* @return 客户端类型,如果不存在则返回PC(兼容旧token)
|
||||
*/
|
||||
public static String getClientType(String token) {
|
||||
try {
|
||||
DecodedJWT jwt = JWT.decode(token);
|
||||
String clientType = jwt.getClaim("clientType").asString();
|
||||
// 如果clientType为空,返回默认值PC(兼容旧token)
|
||||
return oConvertUtils.isNotEmpty(clientType) ? clientType : CommonConstant.CLIENT_TYPE_PC;
|
||||
} catch (JWTDecodeException e) {
|
||||
log.warn("解析token中的clientType失败,使用默认值PC:" + e.getMessage());
|
||||
return CommonConstant.CLIENT_TYPE_PC;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据request中的token获取用户账号
|
||||
* 注意:此方法已适配Sa-Token
|
||||
*
|
||||
* @param request
|
||||
* @return
|
||||
@ -202,9 +109,9 @@ public class JwtUtil {
|
||||
}
|
||||
return username;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从session中获取变量
|
||||
* 从session中获取变量
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
@ -215,7 +122,7 @@ public class JwtUtil {
|
||||
String wellNumber = WELL_NUMBER;
|
||||
|
||||
if(key.indexOf(SymbolConstant.RIGHT_CURLY_BRACKET)!=-1){
|
||||
moshi = key.substring(key.indexOf("}")+1);
|
||||
moshi = key.substring(key.indexOf("}")+1);
|
||||
}
|
||||
String returnValue = null;
|
||||
if (key.contains(wellNumber)) {
|
||||
@ -229,16 +136,16 @@ public class JwtUtil {
|
||||
if(returnValue!=null){returnValue = returnValue + moshi;}
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从当前用户中获取变量
|
||||
* 从当前用户中获取变量
|
||||
* @param key
|
||||
* @param user
|
||||
* @return
|
||||
*/
|
||||
public static String getUserSystemData(String key, SysUserCacheInfo user) {
|
||||
//1.优先获取 SysUserCacheInfo
|
||||
if(user==null) {
|
||||
if (user == null) {
|
||||
try {
|
||||
user = JeecgDataAutorUtils.loadUserInfo();
|
||||
} catch (Exception e) {
|
||||
@ -248,82 +155,82 @@ public class JwtUtil {
|
||||
//2.通过shiro获取登录用户信息
|
||||
LoginUser sysUser = null;
|
||||
try {
|
||||
sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
sysUser = (LoginUser) LoginUserUtils.getSessionUser();
|
||||
} catch (Exception e) {
|
||||
log.warn("SecurityUtils.getSubject() 获取用户信息异常:" + e.getMessage());
|
||||
}
|
||||
|
||||
//#{sys_user_code}%
|
||||
String moshi = "";
|
||||
String wellNumber = WELL_NUMBER;
|
||||
if(key.indexOf(SymbolConstant.RIGHT_CURLY_BRACKET)!=-1){
|
||||
moshi = key.substring(key.indexOf("}")+1);
|
||||
String wellNumber = WELL_NUMBER;
|
||||
if (key.indexOf(SymbolConstant.RIGHT_CURLY_BRACKET) != -1) {
|
||||
moshi = key.substring(key.indexOf("}") + 1);
|
||||
}
|
||||
String returnValue = null;
|
||||
//针对特殊标示处理#{sysOrgCode},判断替换
|
||||
if (key.contains(wellNumber)) {
|
||||
key = key.substring(2,key.indexOf("}"));
|
||||
key = key.substring(2, key.indexOf("}"));
|
||||
} else {
|
||||
key = key;
|
||||
}
|
||||
// 是否存在字符串标志
|
||||
boolean multiStr;
|
||||
if(oConvertUtils.isNotEmpty(key) && key.trim().matches("^\\[\\w+]$")){
|
||||
key = key.substring(1,key.length()-1);
|
||||
if (oConvertUtils.isNotEmpty(key) && key.trim().matches("^\\[\\w+]$")) {
|
||||
key = key.substring(1, key.length() - 1);
|
||||
multiStr = true;
|
||||
} else {
|
||||
multiStr = false;
|
||||
}
|
||||
multiStr = false;
|
||||
}
|
||||
//替换为当前系统时间(年月日)
|
||||
if (key.equals(DataBaseConstant.SYS_DATE)|| key.toLowerCase().equals(DataBaseConstant.SYS_DATE_TABLE)) {
|
||||
if (key.equals(DataBaseConstant.SYS_DATE) || key.toLowerCase().equals(DataBaseConstant.SYS_DATE_TABLE)) {
|
||||
returnValue = DateUtils.formatDate();
|
||||
}
|
||||
//替换为当前系统时间(年月日时分秒)
|
||||
else if (key.equals(DataBaseConstant.SYS_TIME)|| key.toLowerCase().equals(DataBaseConstant.SYS_TIME_TABLE)) {
|
||||
else if (key.equals(DataBaseConstant.SYS_TIME) || key.toLowerCase().equals(DataBaseConstant.SYS_TIME_TABLE)) {
|
||||
returnValue = DateUtils.now();
|
||||
}
|
||||
//流程状态默认值(默认未发起)
|
||||
else if (key.equals(DataBaseConstant.BPM_STATUS)|| key.toLowerCase().equals(DataBaseConstant.BPM_STATUS_TABLE)) {
|
||||
else if (key.equals(DataBaseConstant.BPM_STATUS) || key.toLowerCase().equals(DataBaseConstant.BPM_STATUS_TABLE)) {
|
||||
returnValue = "1";
|
||||
}
|
||||
|
||||
//后台任务获取用户信息异常,导致程序中断
|
||||
if(sysUser==null && user==null){
|
||||
if (sysUser == null && user == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
//替换为系统登录用户帐号
|
||||
if (key.equals(DataBaseConstant.SYS_USER_CODE)|| key.toLowerCase().equals(DataBaseConstant.SYS_USER_CODE_TABLE)) {
|
||||
if(user==null) {
|
||||
if (key.equals(DataBaseConstant.SYS_USER_CODE) || key.toLowerCase().equals(DataBaseConstant.SYS_USER_CODE_TABLE)) {
|
||||
if (user == null) {
|
||||
returnValue = sysUser.getUsername();
|
||||
}else {
|
||||
} else {
|
||||
returnValue = user.getSysUserCode();
|
||||
}
|
||||
}
|
||||
|
||||
// 替换为系统登录用户ID
|
||||
else if (key.equals(DataBaseConstant.SYS_USER_ID) || key.equalsIgnoreCase(DataBaseConstant.SYS_USER_ID_TABLE)) {
|
||||
if(user==null) {
|
||||
if (user == null) {
|
||||
returnValue = sysUser.getId();
|
||||
}else {
|
||||
} else {
|
||||
returnValue = user.getSysUserId();
|
||||
}
|
||||
}
|
||||
|
||||
//替换为系统登录用户真实名字
|
||||
else if (key.equals(DataBaseConstant.SYS_USER_NAME)|| key.toLowerCase().equals(DataBaseConstant.SYS_USER_NAME_TABLE)) {
|
||||
if(user==null) {
|
||||
else if (key.equals(DataBaseConstant.SYS_USER_NAME) || key.toLowerCase().equals(DataBaseConstant.SYS_USER_NAME_TABLE)) {
|
||||
if (user == null) {
|
||||
returnValue = sysUser.getRealname();
|
||||
}else {
|
||||
} else {
|
||||
returnValue = user.getSysUserName();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//替换为系统用户登录所使用的机构编码
|
||||
else if (key.equals(DataBaseConstant.SYS_ORG_CODE)|| key.toLowerCase().equals(DataBaseConstant.SYS_ORG_CODE_TABLE)) {
|
||||
if(user==null) {
|
||||
else if (key.equals(DataBaseConstant.SYS_ORG_CODE) || key.toLowerCase().equals(DataBaseConstant.SYS_ORG_CODE_TABLE)) {
|
||||
if (user == null) {
|
||||
returnValue = sysUser.getOrgCode();
|
||||
}else {
|
||||
} else {
|
||||
returnValue = user.getSysOrgCode();
|
||||
}
|
||||
}
|
||||
@ -338,19 +245,15 @@ public class JwtUtil {
|
||||
}
|
||||
|
||||
//替换为系统用户所拥有的所有机构编码
|
||||
else if (key.equals(DataBaseConstant.SYS_MULTI_ORG_CODE)|| key.toLowerCase().equals(DataBaseConstant.SYS_MULTI_ORG_CODE_TABLE)) {
|
||||
if(user==null){
|
||||
//TODO 暂时使用用户登录部门,存在逻辑缺陷,不是用户所拥有的部门
|
||||
else if (key.equals(DataBaseConstant.SYS_MULTI_ORG_CODE) || key.toLowerCase().equals(DataBaseConstant.SYS_MULTI_ORG_CODE_TABLE)) {
|
||||
if (user == null) {
|
||||
returnValue = sysUser.getOrgCode();
|
||||
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||
returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
|
||||
}else{
|
||||
if(user.isOneDepart()) {
|
||||
} else {
|
||||
if (user.isOneDepart()) {
|
||||
returnValue = user.getSysMultiOrgCode().get(0);
|
||||
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||
returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
|
||||
}else {
|
||||
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||
} else {
|
||||
returnValue = user.getSysMultiOrgCode().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(orgCode -> {
|
||||
@ -374,20 +277,17 @@ public class JwtUtil {
|
||||
}
|
||||
}
|
||||
|
||||
// 代码逻辑说明: 多租户ID作为系统变量
|
||||
else if (key.equals(TenantConstant.TENANT_ID) || key.toLowerCase().equals(TenantConstant.TENANT_ID_TABLE)){
|
||||
// 多租户ID作为系统变量
|
||||
else if (key.equals(TenantConstant.TENANT_ID) || key.toLowerCase().equals(TenantConstant.TENANT_ID_TABLE)) {
|
||||
try {
|
||||
returnValue = SpringContextUtils.getHttpServletRequest().getHeader(CommonConstant.TENANT_ID);
|
||||
} catch (Exception e) {
|
||||
log.warn("获取系统租户异常:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
if(returnValue!=null){returnValue = returnValue + moshi;}
|
||||
if (returnValue != null) {
|
||||
returnValue = returnValue + moshi;
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
// public static void main(String[] args) {
|
||||
// String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NjUzMzY1MTMsInVzZXJuYW1lIjoiYWRtaW4ifQ.xjhud_tWCNYBOg_aRlMgOdlZoWFFKB_givNElHNw3X0";
|
||||
// System.out.println(JwtUtil.getUsername(token));
|
||||
// }
|
||||
}
|
||||
|
||||
@ -3,7 +3,9 @@ package org.jeecg.common.system.util;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.system.annotation.EnumDict;
|
||||
import org.jeecg.common.system.vo.DictModel;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||
@ -11,7 +13,6 @@ import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
|
||||
import org.springframework.core.type.classreading.MetadataReader;
|
||||
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
|
||||
@ -182,10 +183,10 @@ public class ResourceUtil {
|
||||
for (DictModel dm : dictItemList) {
|
||||
String value = dm.getValue();
|
||||
if (keySet.contains(value)) {
|
||||
// 修复bug:获取或创建该dictCode对应的list,而不是每次都创建新的list
|
||||
List<DictModel> list = map.computeIfAbsent(code, k -> new ArrayList<>());
|
||||
List<DictModel> list = new ArrayList<>();
|
||||
list.add(new DictModel(value, dm.getText()));
|
||||
//break;
|
||||
map.put(code, list);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,9 +56,7 @@ public class CommonUtils {
|
||||
public static String uploadOnlineImage(byte[] data,String basePath,String bizPath,String uploadType){
|
||||
String dbPath = null;
|
||||
String fileName = "image" + Math.round(Math.random() * 100000000000L);
|
||||
//update-begin---author:wangshuai---date:2026-01-08---for:【QQYUN-14535】ai生成图片的后缀不一致的,导致不展示---
|
||||
fileName += "." + PoiPublicUtil.getFileExtendName(data).toLowerCase();
|
||||
//update-end---author:wangshuai---date:2026-01-08---for:【QQYUN-14535】ai生成图片的后缀不一致的,导致不展示---
|
||||
fileName += "." + PoiPublicUtil.getFileExtendName(data);
|
||||
try {
|
||||
if(CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)){
|
||||
File file = new File(basePath + File.separator + bizPath + File.separator );
|
||||
|
||||
@ -0,0 +1,175 @@
|
||||
package org.jeecg.common.util;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
|
||||
/**
|
||||
* 登录用户工具类
|
||||
* 替代原有的Shiro SecurityUtils工具类
|
||||
* @author jeecg-boot
|
||||
*/
|
||||
@Slf4j
|
||||
public class LoginUserUtils {
|
||||
|
||||
/**
|
||||
* Session中存储登录用户信息的key
|
||||
*/
|
||||
private static final String SESSION_KEY_LOGIN_USER = "loginUser";
|
||||
|
||||
/**
|
||||
* 执行登录并设置用户信息到Session(推荐)
|
||||
*
|
||||
* <p>此方法会:
|
||||
* <ul>
|
||||
* <li>1. 调用 StpUtil.login(username) 生成token和session</li>
|
||||
* <li>2. 将 LoginUser 存入 Session 缓存(清除不必要的字段(密码等15个字段)</li>
|
||||
* <li>3. 返回生成的 token</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param sysUser 完整的用户对象(从数据库查询得到)
|
||||
* @return 生成的 token
|
||||
*/
|
||||
public static String doLogin(LoginUser sysUser) {
|
||||
if (sysUser == null) {
|
||||
throw new IllegalArgumentException("用户对象不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 获取 username
|
||||
String username = sysUser.getUsername();
|
||||
|
||||
if (username == null || username.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("用户名不能为空");
|
||||
}
|
||||
|
||||
// 2. Sa-Token 登录(使用 username 作为 loginId)
|
||||
StpUtil.login(username);
|
||||
|
||||
// 3. 用户信息到 LoginUser 并存入 Session
|
||||
setSessionUser(sysUser);
|
||||
|
||||
// 4. 返回生成的 token
|
||||
return StpUtil.getTokenValue();
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("登录失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户信息
|
||||
*
|
||||
* <p>说明:
|
||||
* <ul>
|
||||
* <li>对于需要认证的接口:Sa-Token Filter 已经校验过登录状态,此方法必然能获取到用户</li>
|
||||
* <li>对于已排除拦截的接口:如果未登录或获取失败则返回 null,由业务代码自行判断处理</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return 登录用户对象,如果未登录或session中没有则返回null
|
||||
*/
|
||||
public static LoginUser getSessionUser() {
|
||||
// 尝试从Sa-Token的Session中获取用户信息
|
||||
Object loginUser = StpUtil.getSession().get(SESSION_KEY_LOGIN_USER);
|
||||
if (loginUser instanceof LoginUser) {
|
||||
return (LoginUser) loginUser;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据指定的 token 获取登录用户信息
|
||||
*
|
||||
* <p>适用场景:已排除拦截的接口(如 WebSocket),需要显式传入 token 来获取用户信息
|
||||
*
|
||||
* <p>实现方式:临时切换到该 token 对应的会话,然后获取用户信息
|
||||
*
|
||||
* @param token JWT token
|
||||
* @return 登录用户对象,如果 token 无效或session中没有则返回null
|
||||
*/
|
||||
public static LoginUser getSessionUser(String token) {
|
||||
try {
|
||||
// 根据 token 获取登录ID
|
||||
Object loginId = StpUtil.getLoginIdByToken(token);
|
||||
if (loginId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 临时切换到该 token 对应的登录会话
|
||||
StpUtil.switchTo(loginId);
|
||||
|
||||
// 直接调用无参方法获取用户信息
|
||||
return getSessionUser();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.debug("根据token获取用户信息失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置当前登录用户信息到Session
|
||||
*
|
||||
* <p>为减少 Redis 存储和保障安全,只保留必要的核心字段:
|
||||
* <ul>
|
||||
* <li>id, username, realname - 基础用户信息</li>
|
||||
* <li>orgCode, orgId, departIds - 部门和数据权限</li>
|
||||
* <li>roleCode - 角色权限</li>
|
||||
* <li>loginTenantId, relTenantIds - 多租户</li>
|
||||
* <li>avatar - 用户头像</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>⚠️ 注意:调用此方法前需要先调用 StpUtil.login()
|
||||
*
|
||||
* @param loginUser 登录用户对象
|
||||
*/
|
||||
public static void setSessionUser(LoginUser loginUser) {
|
||||
if (loginUser == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ⚠️ 安全与性能:清除不必要的字段,减少 Redis 存储
|
||||
loginUser.setPassword(null); // 密码(安全)
|
||||
loginUser.setWorkNo(null); // 工号
|
||||
loginUser.setBirthday(null); // 生日
|
||||
loginUser.setSex(null); // 性别
|
||||
loginUser.setEmail(null); // 邮箱
|
||||
loginUser.setPhone(null); // 手机号
|
||||
loginUser.setStatus(null); // 状态
|
||||
loginUser.setDelFlag(null); // 删除标志
|
||||
loginUser.setActivitiSync(null); // 工作流同步
|
||||
loginUser.setCreateTime(null); // 创建时间
|
||||
loginUser.setUserIdentity(null); // 用户身份
|
||||
loginUser.setPost(null); // 职务
|
||||
loginUser.setTelephone(null); // 座机
|
||||
loginUser.setRelTenantIds(null); // 关联租户
|
||||
loginUser.setMainDepPostId(null); // 主岗位
|
||||
|
||||
StpUtil.getSession().set(SESSION_KEY_LOGIN_USER, loginUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户名(推荐使用此方法,语义更清晰)
|
||||
* @return 用户名(username)
|
||||
*/
|
||||
public static String getUsername() {
|
||||
return StpUtil.getLoginIdAsString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已登录
|
||||
* @return true-已登录,false-未登录
|
||||
*/
|
||||
public static boolean isLogin() {
|
||||
return StpUtil.isLogin();
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
public static void logout() {
|
||||
StpUtil.logout();
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ public class RestUtil {
|
||||
|
||||
public static String getBaseUrl() {
|
||||
String basepath = getDomain() + getPath();
|
||||
log.debug(" RestUtil.getBaseUrl: " + basepath);
|
||||
log.info(" RestUtil.getBaseUrl: " + basepath);
|
||||
return basepath;
|
||||
}
|
||||
|
||||
@ -199,7 +199,7 @@ public class RestUtil {
|
||||
* @return ResponseEntity<responseType>
|
||||
*/
|
||||
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers, JSONObject variables, Object params, Class<T> responseType) {
|
||||
log.debug(" RestUtil --- request --- url = "+ url);
|
||||
log.info(" RestUtil --- request --- url = "+ url);
|
||||
if (StringUtils.isEmpty(url)) {
|
||||
throw new RuntimeException("url 不能为空");
|
||||
}
|
||||
@ -230,7 +230,7 @@ public class RestUtil {
|
||||
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
|
||||
if (current == null || !current.equals(String.valueOf(contentLength))) {
|
||||
headers.setContentLength(contentLength);
|
||||
log.debug(" RestUtil --- request --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
|
||||
log.info(" RestUtil --- request --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
|
||||
}
|
||||
}
|
||||
// 发送请求
|
||||
@ -252,7 +252,7 @@ public class RestUtil {
|
||||
*/
|
||||
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers,
|
||||
JSONObject variables, Object params, Class<T> responseType, int timeout) {
|
||||
log.debug(" RestUtil --- request --- url = "+ url + ", timeout = " + timeout);
|
||||
log.info(" RestUtil --- request --- url = "+ url + ", timeout = " + timeout);
|
||||
|
||||
if (StringUtils.isEmpty(url)) {
|
||||
throw new RuntimeException("url 不能为空");
|
||||
@ -302,7 +302,7 @@ public class RestUtil {
|
||||
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
|
||||
if (current == null || !current.equals(String.valueOf(contentLength))) {
|
||||
headers.setContentLength(contentLength);
|
||||
log.debug(" RestUtil --- request(timeout) --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
|
||||
log.info(" RestUtil --- request(timeout) --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
package org.jeecg.common.util;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.mgt.SecurityManager;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* @date 2025-09-04
|
||||
* @author scott
|
||||
*
|
||||
* @Description: 支持shiro的API,获取当前登录人方法的线程池
|
||||
*/
|
||||
public class ShiroThreadPoolExecutor extends ThreadPoolExecutor {
|
||||
|
||||
public ShiroThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
|
||||
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Runnable command) {
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
SecurityManager securityManager = SecurityUtils.getSecurityManager();
|
||||
super.execute(() -> {
|
||||
try {
|
||||
ThreadContext.bind(securityManager);
|
||||
ThreadContext.bind(subject);
|
||||
command.run();
|
||||
} finally {
|
||||
ThreadContext.unbindSubject();
|
||||
ThreadContext.unbindSecurityManager();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
package org.jeecg.common.util;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.common.api.CommonAPI;
|
||||
@ -87,75 +88,42 @@ public class TokenUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Token
|
||||
* 验证Token(已重写为Sa-Token实现)
|
||||
*/
|
||||
public static boolean verifyToken(HttpServletRequest request, CommonAPI commonApi, RedisUtil redisUtil) {
|
||||
public static boolean verifyToken(HttpServletRequest request, CommonAPI commonApi) {
|
||||
log.debug(" -- url --" + request.getRequestURL());
|
||||
String token = getTokenByRequest(request);
|
||||
return TokenUtils.verifyToken(token, commonApi, redisUtil);
|
||||
return TokenUtils.verifyToken(token, commonApi);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Token
|
||||
* 验证Token(已重写为Sa-Token实现)
|
||||
*/
|
||||
public static boolean verifyToken(String token, CommonAPI commonApi, RedisUtil redisUtil) {
|
||||
public static boolean verifyToken(String token, CommonAPI commonApi) {
|
||||
if (StringUtils.isBlank(token)) {
|
||||
throw new JeecgBoot401Exception("token不能为空!");
|
||||
}
|
||||
|
||||
// 解密获得username,用于和数据库进行对比
|
||||
String username = JwtUtil.getUsername(token);
|
||||
// 使用Sa-Token校验token
|
||||
Object username = StpUtil.getLoginIdByToken(token);
|
||||
if (username == null) {
|
||||
throw new JeecgBoot401Exception("token非法无效!");
|
||||
}
|
||||
|
||||
// 查询用户信息
|
||||
LoginUser user = TokenUtils.getLoginUser(username, commonApi, redisUtil);
|
||||
//LoginUser user = commonApi.getUserByName(username);
|
||||
LoginUser user = commonApi.getUserByName(username.toString());
|
||||
if (user == null) {
|
||||
throw new JeecgBoot401Exception("用户不存在!");
|
||||
}
|
||||
|
||||
// 判断用户状态
|
||||
if (user.getStatus() != 1) {
|
||||
throw new JeecgBoot401Exception("账号已被锁定,请联系管理员!");
|
||||
}
|
||||
// 校验token是否超时失效 & 或者账号密码是否错误
|
||||
if (!jwtTokenRefresh(token, username, user.getPassword(), redisUtil)) {
|
||||
// 用户登录Token过期提示信息
|
||||
String userLoginTokenErrorMsg = oConvertUtils.getString(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN_ERROR_MSG + token));
|
||||
throw new JeecgBoot401Exception(oConvertUtils.isEmpty(userLoginTokenErrorMsg)? CommonConstant.TOKEN_IS_INVALID_MSG: userLoginTokenErrorMsg);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新token(保证用户在线操作不掉线)
|
||||
* @param token
|
||||
* @param userName
|
||||
* @param passWord
|
||||
* @param redisUtil
|
||||
* @return
|
||||
*/
|
||||
private static boolean jwtTokenRefresh(String token, String userName, String passWord, RedisUtil redisUtil) {
|
||||
String cacheToken = oConvertUtils.getString(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
|
||||
if (oConvertUtils.isNotEmpty(cacheToken)) {
|
||||
// 校验token有效性
|
||||
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
|
||||
// 从token中解析客户端类型,保持续期时使用相同的客户端类型
|
||||
String clientType = JwtUtil.getClientType(token);
|
||||
String newAuthorization = JwtUtil.sign(userName, passWord, clientType);
|
||||
// 根据客户端类型设置对应的缓存有效时间
|
||||
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
|
||||
? JwtUtil.APP_EXPIRE_TIME * 2 / 1000
|
||||
: JwtUtil.EXPIRE_TIME * 2 / 1000;
|
||||
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
|
||||
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, expireTime);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录用户
|
||||
*
|
||||
@ -181,4 +149,5 @@ public class TokenUtils {
|
||||
}
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
package org.jeecg.common.util.encryption;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.lang.codec.Base64;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* AES 工具 (兼容历史 NoPadding + 新 PKCS5Padding)
|
||||
@ -23,7 +24,7 @@ public class AesEncryptUtil {
|
||||
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
||||
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
|
||||
byte[] plain = cipher.doFinal(Base64.decode(cipherBase64));
|
||||
byte[] plain = cipher.doFinal(Base64.getDecoder().decode(cipherBase64));
|
||||
return new String(plain, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@ -33,7 +34,7 @@ public class AesEncryptUtil {
|
||||
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
||||
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
|
||||
byte[] data = cipher.doFinal(Base64.decode(cipherBase64));
|
||||
byte[] data = cipher.doFinal(Base64.getDecoder().decode(cipherBase64));
|
||||
return new String(data, StandardCharsets.UTF_8)
|
||||
.replace("\u0000",""); // 旧填充 0
|
||||
}
|
||||
@ -93,7 +94,7 @@ public class AesEncryptUtil {
|
||||
IvParameterSpec ivspec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
|
||||
byte[] encrypted = cipher.doFinal(plaintext);
|
||||
return Base64.encodeToString(encrypted);
|
||||
return Base64.getEncoder().encodeToString(encrypted);
|
||||
}catch(Exception e){
|
||||
throw new IllegalStateException("legacy encrypt error", e);
|
||||
}
|
||||
|
||||
@ -1231,26 +1231,5 @@ public class oConvertUtils {
|
||||
.filter(name -> beanWrapper.getPropertyValue(name) == null)
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* String转换long类型
|
||||
*
|
||||
* @param v
|
||||
* @param def
|
||||
* @return
|
||||
*/
|
||||
public static long getLong(Object v, long def) {
|
||||
if (v == null) {
|
||||
return def;
|
||||
};
|
||||
if (v instanceof Number) {
|
||||
return ((Number) v).longValue();
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(v.toString());
|
||||
} catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
package org.jeecg.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* ai配置类,通用的配置可以放到这里面
|
||||
*
|
||||
* @Author: wangshuai
|
||||
* @Date: 2025/12/17 14:00
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = AiRagConfigBean.PREFIX)
|
||||
public class AiRagConfigBean {
|
||||
public static final String PREFIX = "jeecg.airag";
|
||||
|
||||
/**
|
||||
* 敏感节点
|
||||
* stdio mpc命令行功能开启,sql:AI流程SQL节点开启
|
||||
*/
|
||||
private String allowSensitiveNodes = "";
|
||||
}
|
||||
@ -12,6 +12,7 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author eightmonth@qq.com
|
||||
* 启动程序修改DruidWallConfig配置
|
||||
* 允许SELECT语句的WHERE子句是一个永真条件
|
||||
* @author eightmonth
|
||||
|
||||
@ -136,7 +136,7 @@ public class Swagger3Config implements WebMvcConfigurer {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("JeecgBoot 后台服务API接口文档")
|
||||
.version("3.9.1")
|
||||
.version("3.9.0")
|
||||
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
|
||||
.description("后台API接口")
|
||||
.termsOfService("NO terms of service")
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
package org.jeecg.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
|
||||
/**
|
||||
* 任务调度器配置
|
||||
* 提供 ThreadPoolTaskScheduler Bean 用于 AI RAG 流程调度等功能
|
||||
* 仅当容器中不存在 ThreadPoolTaskScheduler 时才创建
|
||||
*
|
||||
* @author jeecg
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class TaskSchedulerConfig {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ThreadPoolTaskScheduler.class)
|
||||
public ThreadPoolTaskScheduler taskScheduler() {
|
||||
log.info("初始化定时任务调度器 ThreadPoolTaskScheduler");
|
||||
|
||||
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
scheduler.setPoolSize(10);
|
||||
scheduler.setThreadNamePrefix("airag-scheduler-");
|
||||
scheduler.setWaitForTasksToCompleteOnShutdown(true);
|
||||
scheduler.setAwaitTerminationSeconds(60);
|
||||
return scheduler;
|
||||
}
|
||||
}
|
||||
@ -24,23 +24,18 @@ public class WebsocketFilter implements Filter {
|
||||
|
||||
private static CommonAPI commonApi;
|
||||
|
||||
private static RedisUtil redisUtil;
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
|
||||
if (commonApi == null) {
|
||||
commonApi = SpringContextUtils.getBean(CommonAPI.class);
|
||||
}
|
||||
if (redisUtil == null) {
|
||||
redisUtil = SpringContextUtils.getBean(RedisUtil.class);
|
||||
}
|
||||
HttpServletRequest request = (HttpServletRequest)servletRequest;
|
||||
String token = request.getHeader(TOKEN_KEY);
|
||||
|
||||
log.debug("Websocket连接 Token安全校验,Path = {},token:{}", request.getRequestURI(), token);
|
||||
|
||||
try {
|
||||
TokenUtils.verifyToken(token, commonApi, redisUtil);
|
||||
TokenUtils.verifyToken(token, commonApi);
|
||||
} catch (Exception exception) {
|
||||
//log.error("Websocket连接 Token安全校验失败,IP:{}, Token:{}, Path = {},异常:{}", oConvertUtils.getIpAddrByRequest(request), token, request.getRequestURI(), exception.getMessage());
|
||||
log.debug("Websocket连接 Token安全校验失败,IP:{}, Token:{}, Path = {},异常:{}", oConvertUtils.getIpAddrByRequest(request), token, request.getRequestURI(), exception.getMessage());
|
||||
|
||||
@ -2,7 +2,7 @@ package org.jeecg.config.firewall.interceptor;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.jeecg.common.util.LoginUserUtils;
|
||||
import org.jeecg.common.api.CommonAPI;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
@ -68,7 +68,7 @@ public class LowCodeModeInterceptor implements HandlerInterceptor {
|
||||
if (jeecgBaseConfig.getFirewall()!=null && LowCodeModeInterceptor.LOW_CODE_MODE_PROD.equals(jeecgBaseConfig.getFirewall().getLowCodeMode())) {
|
||||
String requestURI = request.getRequestURI().substring(request.getContextPath().length());
|
||||
log.info("低代码模式,拦截请求路径:" + requestURI);
|
||||
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
LoginUser loginUser = LoginUserUtils.getSessionUser();
|
||||
Set<String> hasRoles = null;
|
||||
if (loginUser == null) {
|
||||
loginUser = commonAPI.getUserByName(JwtUtil.getUserNameByToken(SpringContextUtils.getHttpServletRequest()));
|
||||
|
||||
@ -6,7 +6,7 @@ import org.apache.ibatis.executor.Executor;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.mapping.SqlCommandType;
|
||||
import org.apache.ibatis.plugin.*;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.jeecg.common.util.LoginUserUtils;
|
||||
import org.jeecg.common.config.TenantContext;
|
||||
import org.jeecg.common.constant.TenantConstant;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
@ -189,7 +189,7 @@ public class MybatisInterceptor implements Interceptor {
|
||||
private LoginUser getLoginUser() {
|
||||
LoginUser sysUser = null;
|
||||
try {
|
||||
sysUser = SecurityUtils.getSubject().getPrincipal() != null ? (LoginUser) SecurityUtils.getSubject().getPrincipal() : null;
|
||||
sysUser = LoginUserUtils.getSessionUser() != null ? LoginUserUtils.getSessionUser() : null;
|
||||
} catch (Exception e) {
|
||||
//e.printStackTrace();
|
||||
sysUser = null;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package org.jeecg.config.shiro;
|
||||
package org.jeecg.config.satoken;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
@ -16,3 +16,4 @@ import java.lang.annotation.Target;
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface IgnoreAuth {
|
||||
}
|
||||
|
||||
@ -0,0 +1,420 @@
|
||||
package org.jeecg.config.satoken;
|
||||
|
||||
import cn.dev33.satoken.context.SaHolder;
|
||||
import cn.dev33.satoken.context.model.SaRequest;
|
||||
import cn.dev33.satoken.exception.NotLoginException;
|
||||
import cn.dev33.satoken.filter.SaServletFilter;
|
||||
import cn.dev33.satoken.interceptor.SaInterceptor;
|
||||
import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
|
||||
import cn.dev33.satoken.router.SaHttpMethod;
|
||||
import cn.dev33.satoken.router.SaRouter;
|
||||
import cn.dev33.satoken.stp.StpLogic;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.CommonAPI;
|
||||
import org.jeecg.common.config.TenantContext;
|
||||
import org.jeecg.common.constant.CacheConstant;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.*;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||
import org.jeecg.config.satoken.ignore.InMemoryIgnoreAuth;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author: jeecg-boot
|
||||
* @description: Sa-Token 配置类
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public class SaTokenConfig implements WebMvcConfigurer {
|
||||
|
||||
@Resource
|
||||
private JeecgBaseConfig jeecgBaseConfig;
|
||||
@Autowired
|
||||
private Environment env;
|
||||
@Autowired
|
||||
private CommonAPI commonAPI;
|
||||
@Autowired
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
/**
|
||||
* Sa-Token 整合 jwt (Simple 模式)
|
||||
* 使用JWT-Simple模式生成标准JWT格式的token
|
||||
* 并支持从URL参数"token"读取token(兼容原系统)
|
||||
*/
|
||||
@Bean
|
||||
@Primary
|
||||
public StpLogic getStpLogicJwt() {
|
||||
return new StpLogicJwtForSimple() {
|
||||
/**
|
||||
* 获取当前请求的 Token 值
|
||||
* 优先级:Header > URL参数token > URL参数X-Access-Token
|
||||
*/
|
||||
@Override
|
||||
public String getTokenValue() {
|
||||
try {
|
||||
SaRequest request = SaHolder.getRequest();
|
||||
|
||||
// 1. 优先从Header中获取
|
||||
String tokenValue = request.getHeader(getConfigOrGlobal().getTokenName());
|
||||
if (oConvertUtils.isNotEmpty(tokenValue)) {
|
||||
return tokenValue;
|
||||
}
|
||||
|
||||
// 2. 从URL参数"token"获取(兼容原系统)
|
||||
tokenValue = request.getParam("token");
|
||||
if (oConvertUtils.isNotEmpty(tokenValue)) {
|
||||
return tokenValue;
|
||||
}
|
||||
|
||||
// 3. 从URL参数"X-Access-Token"获取
|
||||
tokenValue = request.getParam(getConfigOrGlobal().getTokenName());
|
||||
if (oConvertUtils.isNotEmpty(tokenValue)) {
|
||||
return tokenValue;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("获取token失败: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// 4. 如果都没有,使用默认逻辑
|
||||
return super.getTokenValue();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册 Sa-Token 拦截器,打开注解式鉴权功能
|
||||
*/
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
|
||||
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册 Sa-Token 全局过滤器
|
||||
*/
|
||||
@Bean
|
||||
public SaServletFilter getSaServletFilter() {
|
||||
return new SaServletFilter()
|
||||
// 指定 [拦截路由] 与 [放行路由]
|
||||
.addInclude("/**")
|
||||
.setExcludeList(getExcludeUrls())
|
||||
// 认证函数: 每次请求执行
|
||||
.setAuth(obj -> {
|
||||
// 检查是否是免认证路径
|
||||
String servletPath = SaHolder.getRequest().getRequestPath();
|
||||
if (InMemoryIgnoreAuth.contains(servletPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验 token:如果请求中带有 token,先切换到对应的登录会话再校验
|
||||
try {
|
||||
String token = StpUtil.getTokenValue();
|
||||
if (oConvertUtils.isNotEmpty(token)) {
|
||||
// 根据 token 获取 loginId 并切换到对应的登录会话
|
||||
Object loginId = StpUtil.getLoginIdByToken(token);
|
||||
if (loginId != null) {
|
||||
StpUtil.switchTo(loginId);
|
||||
|
||||
// 需要手工自动续签,默认参数auto-renew:true 不好使
|
||||
long activeTimeout = StpUtil.stpLogic.getConfigOrGlobal().getActiveTimeout();
|
||||
if (activeTimeout > 0) {
|
||||
// 获取当前token的活跃剩余时间
|
||||
long tokenActiveTimeout = StpUtil.getTokenActiveTimeout();
|
||||
|
||||
// 如果剩余活跃时间少于总活跃时间的一半,进行续签
|
||||
if (tokenActiveTimeout > 0 && tokenActiveTimeout < (activeTimeout / 2)) {
|
||||
StpUtil.stpLogic.updateLastActiveToNow(token);
|
||||
log.info("【Sa-Token拦截器】Token续签成功,剩余活跃时间: {}秒", tokenActiveTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 如果获取 loginId 失败,说明 token 无效或未登录,让 checkLogin 抛出异常
|
||||
log.debug("切换登录会话失败: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// 最终校验登录状态
|
||||
StpUtil.checkLogin();
|
||||
|
||||
// 租户校验逻辑
|
||||
checkTenantAuthorization();
|
||||
})
|
||||
// 异常处理函数:每次认证函数发生异常时执行此函数
|
||||
.setError(e -> {
|
||||
log.warn("Sa-Token 认证失败:用户未登录或token无效");
|
||||
log.warn("请求路径: {}, Method: {},Token: {}", SaHolder.getRequest().getRequestPath(), SaHolder.getRequest().getMethod(), StpUtil.getTokenValue());
|
||||
|
||||
// 返回401状态码
|
||||
SaHolder.getResponse().setStatus(401).setHeader("Content-Type", "application/json;charset=UTF-8");
|
||||
return org.jeecg.common.system.util.JwtUtil.responseErrorJson(401, CommonConstant.TOKEN_IS_INVALID_MSG);
|
||||
})
|
||||
// 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入)
|
||||
.setBeforeAuth(r -> {
|
||||
// 设置跨域配置
|
||||
Object cloudServer = env.getProperty(CommonConstant.CLOUD_SERVER_KEY);
|
||||
// 如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
|
||||
if (cloudServer == null) {
|
||||
SaHolder.getResponse()
|
||||
// 允许指定域访问跨域资源
|
||||
.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, SaHolder.getRequest().getHeader(HttpHeaders.ORIGIN))
|
||||
// 允许所有请求方式
|
||||
.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS")
|
||||
// 有效时间
|
||||
.setHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "3600")
|
||||
// 允许的header参数
|
||||
.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, SaHolder.getRequest().getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS))
|
||||
// 允许携带凭证
|
||||
.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
|
||||
}
|
||||
|
||||
// OPTIONS预检请求,直接返回
|
||||
SaRouter.match(SaHttpMethod.OPTIONS).free(r2 -> {
|
||||
SaHolder.getResponse().setStatus(HttpStatus.OK.value());
|
||||
});
|
||||
|
||||
// 设置当前线程上下文的租户ID
|
||||
String tenantId = SaHolder.getRequest().getHeader(CommonConstant.TENANT_ID);
|
||||
TenantContext.setTenant(tenantId);
|
||||
log.debug("===【TenantContext 线程设置】=== 请求路径: {}, 租户ID: {}", SaHolder.getRequest().getRequestPath(), tenantId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* spring过滤装饰器 <br/>
|
||||
* 支持异步请求的过滤器装饰
|
||||
*/
|
||||
@Bean
|
||||
public FilterRegistrationBean<SaServletFilter> saTokenFilterRegistration() {
|
||||
FilterRegistrationBean<SaServletFilter> registration = new FilterRegistrationBean<>();
|
||||
registration.setFilter(getSaServletFilter());
|
||||
registration.setName("SaServletFilter");
|
||||
// 支持异步请求
|
||||
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
|
||||
// 拦截所有请求
|
||||
registration.addUrlPatterns("/*");
|
||||
registration.setOrder(1);
|
||||
registration.setAsyncSupported(true); // 支持异步请求
|
||||
return registration;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取排除URL列表
|
||||
*/
|
||||
private List<String> getExcludeUrls() {
|
||||
List<String> excludeUrls = new ArrayList<>();
|
||||
|
||||
// 支持yml方式,配置拦截排除
|
||||
if (jeecgBaseConfig != null && jeecgBaseConfig.getShiro() != null) {
|
||||
String shiroExcludeUrls = jeecgBaseConfig.getShiro().getExcludeUrls();
|
||||
if (oConvertUtils.isNotEmpty(shiroExcludeUrls)) {
|
||||
String[] permissionUrl = shiroExcludeUrls.split(",");
|
||||
excludeUrls.addAll(Arrays.asList(permissionUrl));
|
||||
}
|
||||
}
|
||||
|
||||
// 添加默认排除路径
|
||||
excludeUrls.addAll(Arrays.asList(
|
||||
"/sys/cas/client/validateLogin", // cas验证登录
|
||||
"/sys/randomImage/**", // 登录验证码接口排除
|
||||
"/sys/checkCaptcha", // 登录验证码接口排除
|
||||
"/sys/smsCheckCaptcha", // 短信次数发送太多验证码排除
|
||||
"/sys/login", // 登录接口排除
|
||||
"/sys/mLogin", // 登录接口排除
|
||||
"/sys/logout", // 登出接口排除
|
||||
"/sys/thirdLogin/**", // 第三方登录
|
||||
"/sys/getEncryptedString", // 获取加密串
|
||||
"/sys/sms", // 短信验证码
|
||||
"/sys/phoneLogin", // 手机登录
|
||||
"/sys/user/checkOnlyUser", // 校验用户是否存在
|
||||
"/sys/user/register", // 用户注册
|
||||
"/sys/user/phoneVerification", // 用户忘记密码验证手机号
|
||||
"/sys/user/passwordChange", // 用户更改密码
|
||||
"/auth/2step-code", // 登录验证码
|
||||
"/sys/common/static/**", // 图片预览 & 下载文件不限制token
|
||||
"/sys/common/pdf/**", // pdf预览
|
||||
"/generic/**", // pdf预览需要文件
|
||||
"/sys/getLoginQrcode/**", // 登录二维码
|
||||
"/sys/getQrcodeToken/**", // 监听扫码
|
||||
"/sys/checkAuth", // 授权接口排除
|
||||
"/openapi/call/**", // 开放平台接口排除
|
||||
|
||||
// 排除静态资源后缀
|
||||
"/",
|
||||
"/doc.html",
|
||||
"**/*.js",
|
||||
"**/*.css",
|
||||
"**/*.html",
|
||||
"**/*.svg",
|
||||
"**/*.pdf",
|
||||
"**/*.jpg",
|
||||
"**/*.png",
|
||||
"**/*.gif",
|
||||
"**/*.ico",
|
||||
"**/*.ttf",
|
||||
"**/*.woff",
|
||||
"**/*.woff2",
|
||||
"**/*.glb",
|
||||
"**/*.wasm",
|
||||
"**/*.js.map",
|
||||
"**/*.css.map",
|
||||
|
||||
"/druid/**",
|
||||
"/swagger-ui.html",
|
||||
"/swagger*/**",
|
||||
"/webjars/**",
|
||||
"/v3/**",
|
||||
|
||||
// 排除消息通告查看详情页面(用于第三方APP)
|
||||
"/sys/annountCement/show/**",
|
||||
|
||||
// 积木报表和积木BI排除
|
||||
"/jmreport/**",
|
||||
"/drag/lib/**",
|
||||
"/drag/list/**",
|
||||
"/drag/favicon.ico",
|
||||
"/drag/view",
|
||||
"/drag/page/queryById",
|
||||
"/drag/page/addVisitsNumber",
|
||||
"/drag/page/queryTemplateList",
|
||||
"/drag/share/view/**",
|
||||
"/drag/onlDragDatasetHead/getAllChartData",
|
||||
"/drag/onlDragDatasetHead/getTotalData",
|
||||
"/drag/onlDragDatasetHead/getMapDataByCode",
|
||||
"/drag/onlDragDatasetHead/getTotalDataByCompId",
|
||||
"/drag/mock/json/**",
|
||||
"/drag/onlDragDatasetHead/getDictByCodes",
|
||||
"/drag/onlDragDatasetHead/queryAllById",
|
||||
"/jimubi/view",
|
||||
"/jimubi/share/view/**",
|
||||
|
||||
// 大屏模板例子
|
||||
"/test/bigScreen/**",
|
||||
"/bigscreen/template1/**",
|
||||
"/bigscreen/template2/**",
|
||||
|
||||
// websocket排除
|
||||
"/websocket/**", // 系统通知和公告
|
||||
"/newsWebsocket/**", // CMS模块
|
||||
"/vxeSocket/**", // JVxeTable无痕刷新示例
|
||||
"/dragChannelSocket/**", // 仪表盘(按钮通信)
|
||||
|
||||
// App vue3版本查询版本接口
|
||||
"/sys/version/app3version",
|
||||
|
||||
// 测试模块排除
|
||||
"/test/seata/**",
|
||||
|
||||
// 错误路径排除
|
||||
"/error",
|
||||
|
||||
// 企业微信证书排除
|
||||
"/WW_verify*"
|
||||
));
|
||||
|
||||
return excludeUrls;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验用户的tenant_id和前端传过来的是否一致
|
||||
*
|
||||
* <p>实现逻辑:
|
||||
* <ul>
|
||||
* <li>1. 获取当前登录用户信息</li>
|
||||
* <li>2. 检查用户是否配置了租户信息</li>
|
||||
* <li>3. 获取前端请求头中的租户ID</li>
|
||||
* <li>4. 校验用户所属租户中是否包含当前请求的租户ID</li>
|
||||
* <li>5. 如果校验失败,从数据库重新查询用户信息并再次校验</li>
|
||||
* <li>6. 最终校验失败则抛出异常</li>
|
||||
* </ul>
|
||||
*
|
||||
* @throws NotLoginException 租户授权变更异常
|
||||
*/
|
||||
private void checkTenantAuthorization() {
|
||||
log.debug("------ 租户校验开始 ------");
|
||||
// 如果未开启租户控制,直接返回
|
||||
if (!MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取当前登录用户信息
|
||||
LoginUser loginUser = TokenUtils.getLoginUser(LoginUserUtils.getUsername(), commonAPI, redisUtil);
|
||||
if (loginUser == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String username = loginUser.getUsername();
|
||||
String userTenantIds = loginUser.getRelTenantIds();
|
||||
|
||||
// 如果用户未配置租户信息,直接返回
|
||||
if (oConvertUtils.isEmpty(userTenantIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取前端请求头中的租户ID
|
||||
String loginTenantId = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
|
||||
log.info("登录租户:{}", loginTenantId);
|
||||
log.info("用户拥有那些租户:{}", userTenantIds);
|
||||
|
||||
// 登录用户无租户,前端header中租户ID值为 0
|
||||
String str = "0";
|
||||
if (oConvertUtils.isEmpty(loginTenantId) || str.equals(loginTenantId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String[] userTenantIdsArray = userTenantIds.split(",");
|
||||
if (!oConvertUtils.isIn(loginTenantId, userTenantIdsArray)) {
|
||||
boolean isAuthorization = false;
|
||||
|
||||
//========================================================================
|
||||
// 查询用户信息(如果租户不匹配从数据库中重新查询一次用户信息)
|
||||
String loginUserKey = CacheConstant.SYS_USERS_CACHE + "::" + username;
|
||||
redisUtil.del(loginUserKey);
|
||||
|
||||
LoginUser loginUserFromDb = commonAPI.getUserByName(username);
|
||||
LoginUserUtils.setSessionUser(loginUserFromDb);
|
||||
if (loginUserFromDb != null && oConvertUtils.isNotEmpty(loginUserFromDb.getRelTenantIds())) {
|
||||
String[] newArray = loginUserFromDb.getRelTenantIds().split(",");
|
||||
if (oConvertUtils.isIn(loginTenantId, newArray)) {
|
||||
isAuthorization = true;
|
||||
}
|
||||
}
|
||||
//========================================================================
|
||||
|
||||
if (!isAuthorization) {
|
||||
log.info("租户异常——登录租户:{}", loginTenantId);
|
||||
log.info("租户异常——用户拥有租户组:{}", userTenantIds);
|
||||
throw new NotLoginException("登录租户授权变更,请重新登陆!", StpUtil.TYPE, NotLoginException.KICK_OUT);
|
||||
}
|
||||
}
|
||||
|
||||
}catch (Exception e) {
|
||||
log.error("租户校验异常:{}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,174 @@
|
||||
package org.jeecg.config.satoken;
|
||||
|
||||
import cn.dev33.satoken.dao.SaTokenDao;
|
||||
import cn.dev33.satoken.SaManager;
|
||||
import cn.dev33.satoken.stp.StpInterface;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.CommonAPI;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @description: Sa-Token 权限认证接口实现(带缓存)
|
||||
*
|
||||
* <p>⚠️ 重要说明:</p>
|
||||
* <ul>
|
||||
* <li><strong>Sa-Token 的 StpInterface 默认不提供缓存能力</strong>,需要自己实现缓存逻辑</li>
|
||||
* <li>本实现采用 <strong>[账号id -> 权限/角色列表]</strong> 缓存模型</li>
|
||||
* <li>缓存键格式:
|
||||
* <ul>
|
||||
* <li>用户权限缓存:satoken:user-permission:{username}</li>
|
||||
* <li>用户角色缓存:satoken:user-role:{username}</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li>缓存过期时间:30天</li>
|
||||
* <li>⚠️ 当修改用户的角色或权限时,需要手动清除缓存</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>清除缓存示例:</p>
|
||||
* <pre>
|
||||
* // 清除单个用户的权限和角色缓存
|
||||
* StpInterfaceImpl.clearUserCache("admin");
|
||||
*
|
||||
* // 清除多个用户的缓存
|
||||
* StpInterfaceImpl.clearUserCache(Arrays.asList("admin", "user1", "user2"));
|
||||
* </pre>
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class StpInterfaceImpl implements StpInterface {
|
||||
|
||||
@Lazy
|
||||
@Resource
|
||||
private CommonAPI commonApi;
|
||||
|
||||
/**
|
||||
* 缓存过期时间(秒):30天
|
||||
*/
|
||||
private static final long CACHE_TIMEOUT = 60 * 60 * 24 * 30;
|
||||
|
||||
/**
|
||||
* 权限缓存键前缀
|
||||
*/
|
||||
private static final String PERMISSION_CACHE_PREFIX = "satoken:user-permission:";
|
||||
|
||||
/**
|
||||
* 角色缓存键前缀
|
||||
*/
|
||||
private static final String ROLE_CACHE_PREFIX = "satoken:user-role:";
|
||||
|
||||
/**
|
||||
* 返回一个账号所拥有的权限码集合(带缓存)
|
||||
*
|
||||
* @param loginId 账号id(这里是 username)
|
||||
* @param loginType 账号类型
|
||||
* @return 权限码集合
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<String> getPermissionList(Object loginId, String loginType) {
|
||||
String username = loginId.toString();
|
||||
String cacheKey = PERMISSION_CACHE_PREFIX + username;
|
||||
|
||||
SaTokenDao dao = SaManager.getSaTokenDao();
|
||||
|
||||
// 1. 先从缓存获取
|
||||
List<String> permissionList = (List<String>) dao.getObject(cacheKey);
|
||||
|
||||
if (permissionList == null) {
|
||||
// 2. 缓存不存在,从数据库查询
|
||||
log.warn("权限缓存未命中,查询数据库 [ username={} ]", username);
|
||||
|
||||
String userId = commonApi.getUserIdByName(username);
|
||||
if (userId == null) {
|
||||
log.warn("用户不存在: {}", username);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
Set<String> permissionSet = commonApi.queryUserAuths(userId);
|
||||
permissionList = new ArrayList<>(permissionSet);
|
||||
|
||||
// 3. 将结果缓存起来
|
||||
dao.setObject(cacheKey, permissionList, CACHE_TIMEOUT);
|
||||
log.info("权限已缓存 [ username={}, permissions={} ]", username, permissionList.size());
|
||||
} else {
|
||||
log.debug("权限缓存命中 [ username={}, permissions={} ]", username, permissionList.size());
|
||||
}
|
||||
|
||||
return permissionList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回一个账号所拥有的角色标识集合(带缓存)
|
||||
*
|
||||
* @param loginId 账号id(这里是 username)
|
||||
* @param loginType 账号类型
|
||||
* @return 角色标识集合
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<String> getRoleList(Object loginId, String loginType) {
|
||||
String username = loginId.toString();
|
||||
String cacheKey = ROLE_CACHE_PREFIX + username;
|
||||
|
||||
SaTokenDao dao = SaManager.getSaTokenDao();
|
||||
|
||||
// 1. 先从缓存获取
|
||||
List<String> roleList = (List<String>) dao.getObject(cacheKey);
|
||||
|
||||
if (roleList == null) {
|
||||
// 2. 缓存不存在,从数据库查询
|
||||
log.warn("角色缓存未命中,查询数据库 [ username={} ]", username);
|
||||
|
||||
String userId = commonApi.getUserIdByName(username);
|
||||
if (userId == null) {
|
||||
log.warn("用户不存在: {}", username);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
Set<String> roleSet = commonApi.queryUserRolesById(userId);
|
||||
roleList = new ArrayList<>(roleSet);
|
||||
|
||||
// 3. 将结果缓存起来
|
||||
dao.setObject(cacheKey, roleList, CACHE_TIMEOUT);
|
||||
log.info("角色已缓存 [ username={}, roles={} ]", username, roleList.size());
|
||||
} else {
|
||||
log.debug("角色缓存命中 [ username={}, roles={} ]", username, roleList.size());
|
||||
}
|
||||
|
||||
return roleList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除单个用户的权限和角色缓存
|
||||
* <p>使用场景:修改用户的角色分配后</p>
|
||||
*
|
||||
* @param username 用户名
|
||||
*/
|
||||
public static void clearUserCache(String username) {
|
||||
SaTokenDao dao = SaManager.getSaTokenDao();
|
||||
dao.deleteObject(PERMISSION_CACHE_PREFIX + username);
|
||||
dao.deleteObject(ROLE_CACHE_PREFIX + username);
|
||||
log.info("已清除用户缓存 [ username={} ]", username);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量清除多个用户的权限和角色缓存
|
||||
* <p>使用场景:修改角色权限后,清除拥有该角色的所有用户的缓存</p>
|
||||
*
|
||||
* @param usernameList 用户名列表
|
||||
*/
|
||||
public static void clearUserCache(List<String> usernameList) {
|
||||
SaTokenDao dao = SaManager.getSaTokenDao();
|
||||
for (String username : usernameList) {
|
||||
dao.deleteObject(PERMISSION_CACHE_PREFIX + username);
|
||||
dao.deleteObject(ROLE_CACHE_PREFIX + username);
|
||||
}
|
||||
log.info("已批量清除用户缓存 [ count={} ]", usernameList.size());
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,13 @@
|
||||
package org.jeecg.config.shiro.ignore;
|
||||
package org.jeecg.config.satoken.ignore;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.config.shiro.IgnoreAuth;
|
||||
import org.jeecg.config.satoken.IgnoreAuth;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
@ -33,17 +32,17 @@ public class IgnoreAuthPostProcessor implements InitializingBean {
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
|
||||
List<String> ignoreAuthUrls = new ArrayList<>();
|
||||
|
||||
|
||||
// 优化:直接从HandlerMethod过滤,避免重复扫描
|
||||
requestMappingHandlerMapping.getHandlerMethods().values().stream()
|
||||
.filter(handlerMethod -> handlerMethod.getMethod().isAnnotationPresent(IgnoreAuth.class))
|
||||
.forEach(handlerMethod -> {
|
||||
Class<?> clazz = handlerMethod.getBeanType();
|
||||
Method method = handlerMethod.getMethod();
|
||||
ignoreAuthUrls.addAll(processIgnoreAuthMethod(clazz, method));
|
||||
});
|
||||
.filter(handlerMethod -> handlerMethod.getMethod().isAnnotationPresent(IgnoreAuth.class))
|
||||
.forEach(handlerMethod -> {
|
||||
Class<?> clazz = handlerMethod.getBeanType();
|
||||
Method method = handlerMethod.getMethod();
|
||||
ignoreAuthUrls.addAll(processIgnoreAuthMethod(clazz, method));
|
||||
});
|
||||
|
||||
log.info("Init Token ignoreAuthUrls Config [ 集合 ] :{}", ignoreAuthUrls);
|
||||
if (!CollectionUtils.isEmpty(ignoreAuthUrls)) {
|
||||
@ -60,7 +59,7 @@ public class IgnoreAuthPostProcessor implements InitializingBean {
|
||||
private List<String> processIgnoreAuthMethod(Class<?> clazz, Method method) {
|
||||
RequestMapping base = clazz.getAnnotation(RequestMapping.class);
|
||||
String[] baseUrl = Objects.nonNull(base) ? base.value() : new String[]{};
|
||||
|
||||
|
||||
String[] uri = null;
|
||||
if (method.isAnnotationPresent(RequestMapping.class)) {
|
||||
uri = method.getAnnotation(RequestMapping.class).value();
|
||||
@ -75,7 +74,7 @@ public class IgnoreAuthPostProcessor implements InitializingBean {
|
||||
} else if (method.isAnnotationPresent(PatchMapping.class)) {
|
||||
uri = method.getAnnotation(PatchMapping.class).value();
|
||||
}
|
||||
|
||||
|
||||
return uri != null ? rebuildUrl(baseUrl, uri) : Collections.emptyList();
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package org.jeecg.config.shiro.ignore;
|
||||
package org.jeecg.config.satoken.ignore;
|
||||
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.PathMatcher;
|
||||
@ -6,8 +6,8 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 使用内存存储通过@IgnoreAuth注解的url,配合JwtFilter进行免登录校验
|
||||
* PS:无法使用ThreadLocal进行存储,因为ThreadLocal装载时,JwtFilter已经初始化完毕,导致该类获取ThreadLocal为空
|
||||
* 使用内存存储通过@IgnoreAuth注解的url,配合Sa-Token进行免登录校验
|
||||
* PS:无法使用ThreadLocal进行存储,因为ThreadLocal装载时,Filter已经初始化完毕,导致该类获取ThreadLocal为空
|
||||
* @author eightmonth
|
||||
* @date 2024/4/18 15:02
|
||||
*/
|
||||
@ -15,6 +15,7 @@ public class InMemoryIgnoreAuth {
|
||||
private static final List<String> IGNORE_AUTH_LIST = new ArrayList<>();
|
||||
|
||||
private static PathMatcher MATCHER = new AntPathMatcher();
|
||||
|
||||
public InMemoryIgnoreAuth() {}
|
||||
|
||||
public static void set(List<String> list) {
|
||||
@ -31,11 +32,11 @@ public class InMemoryIgnoreAuth {
|
||||
|
||||
public static boolean contains(String url) {
|
||||
for (String ignoreAuth : IGNORE_AUTH_LIST) {
|
||||
if(MATCHER.match(ignoreAuth,url)){
|
||||
if(MATCHER.match(ignoreAuth, url)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
package org.jeecg.config.shiro;
|
||||
|
||||
import org.apache.shiro.authc.AuthenticationToken;
|
||||
|
||||
/**
|
||||
* @Author Scott
|
||||
* @create 2018-07-12 15:19
|
||||
* @desc
|
||||
**/
|
||||
public class JwtToken implements AuthenticationToken {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private String token;
|
||||
|
||||
public JwtToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
@ -1,395 +0,0 @@
|
||||
package org.jeecg.config.shiro;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.Filter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
||||
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
|
||||
import org.apache.shiro.mgt.DefaultSubjectDAO;
|
||||
import org.apache.shiro.mgt.SecurityManager;
|
||||
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
|
||||
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
|
||||
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
|
||||
import org.apache.shiro.spring.web.ShiroUrlPathHelper;
|
||||
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
|
||||
import org.crazycake.shiro.*;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.config.shiro.filters.CustomShiroFilterFactoryBean;
|
||||
import org.jeecg.config.shiro.filters.JwtFilter;
|
||||
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.*;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.filter.DelegatingFilterProxy;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
import redis.clients.jedis.HostAndPort;
|
||||
import redis.clients.jedis.JedisCluster;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author: Scott
|
||||
* @date: 2018/2/7
|
||||
* @description: shiro 配置类
|
||||
*/
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class ShiroConfig {
|
||||
|
||||
@Resource
|
||||
private LettuceConnectionFactory lettuceConnectionFactory;
|
||||
@Autowired
|
||||
private Environment env;
|
||||
@Resource
|
||||
private JeecgBaseConfig jeecgBaseConfig;
|
||||
@Autowired(required = false)
|
||||
private RedisProperties redisProperties;
|
||||
|
||||
/**
|
||||
* Filter Chain定义说明
|
||||
*
|
||||
* 1、一个URL可以配置多个Filter,使用逗号分隔
|
||||
* 2、当设置多个过滤器时,全部验证通过,才视为通过
|
||||
* 3、部分过滤器可指定参数,如perms,roles
|
||||
*/
|
||||
@Bean("shiroFilterFactoryBean")
|
||||
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
|
||||
CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean();
|
||||
shiroFilterFactoryBean.setSecurityManager(securityManager);
|
||||
// 拦截器
|
||||
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
|
||||
|
||||
//支持yml方式,配置拦截排除
|
||||
if(jeecgBaseConfig!=null && jeecgBaseConfig.getShiro()!=null){
|
||||
String shiroExcludeUrls = jeecgBaseConfig.getShiro().getExcludeUrls();
|
||||
if(oConvertUtils.isNotEmpty(shiroExcludeUrls)){
|
||||
String[] permissionUrl = shiroExcludeUrls.split(",");
|
||||
for(String url : permissionUrl){
|
||||
filterChainDefinitionMap.put(url,"anon");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 配置不会被拦截的链接 顺序判断
|
||||
filterChainDefinitionMap.put("/sys/cas/client/validateLogin", "anon"); //cas验证登录
|
||||
filterChainDefinitionMap.put("/sys/randomImage/**", "anon"); //登录验证码接口排除
|
||||
filterChainDefinitionMap.put("/sys/checkCaptcha", "anon"); //登录验证码接口排除
|
||||
filterChainDefinitionMap.put("/sys/smsCheckCaptcha", "anon"); //短信次数发送太多验证码排除
|
||||
filterChainDefinitionMap.put("/sys/login", "anon"); //登录接口排除
|
||||
filterChainDefinitionMap.put("/sys/mLogin", "anon"); //登录接口排除
|
||||
filterChainDefinitionMap.put("/sys/logout", "anon"); //登出接口排除
|
||||
filterChainDefinitionMap.put("/sys/thirdLogin/**", "anon"); //第三方登录
|
||||
filterChainDefinitionMap.put("/sys/getEncryptedString", "anon"); //获取加密串
|
||||
filterChainDefinitionMap.put("/sys/sms", "anon");//短信验证码
|
||||
filterChainDefinitionMap.put("/sys/phoneLogin", "anon");//手机登录
|
||||
filterChainDefinitionMap.put("/sys/user/checkOnlyUser", "anon");//校验用户是否存在
|
||||
filterChainDefinitionMap.put("/sys/user/register", "anon");//用户注册
|
||||
filterChainDefinitionMap.put("/sys/user/phoneVerification", "anon");//用户忘记密码验证手机号
|
||||
filterChainDefinitionMap.put("/sys/user/passwordChange", "anon");//用户更改密码
|
||||
filterChainDefinitionMap.put("/auth/2step-code", "anon");//登录验证码
|
||||
filterChainDefinitionMap.put("/sys/common/static/**", "anon");//图片预览 &下载文件不限制token
|
||||
filterChainDefinitionMap.put("/sys/common/pdf/**", "anon");//pdf预览
|
||||
|
||||
//filterChainDefinitionMap.put("/sys/common/view/**", "anon");//图片预览不限制token
|
||||
//filterChainDefinitionMap.put("/sys/common/download/**", "anon");//文件下载不限制token
|
||||
filterChainDefinitionMap.put("/generic/**", "anon");//pdf预览需要文件
|
||||
|
||||
filterChainDefinitionMap.put("/sys/getLoginQrcode/**", "anon"); //登录二维码
|
||||
filterChainDefinitionMap.put("/sys/getQrcodeToken/**", "anon"); //监听扫码
|
||||
filterChainDefinitionMap.put("/sys/checkAuth", "anon"); //授权接口排除
|
||||
filterChainDefinitionMap.put("/openapi/call/**", "anon"); // 开放平台接口排除
|
||||
|
||||
// 代码逻辑说明: 排除静态资源后缀
|
||||
filterChainDefinitionMap.put("/", "anon");
|
||||
filterChainDefinitionMap.put("/doc.html", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.js", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.css", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.html", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.svg", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.pdf", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.jpg", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.png", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.gif", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.ico", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.ttf", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.woff", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.woff2", "anon");
|
||||
|
||||
filterChainDefinitionMap.put("/**/*.glb", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.wasm", "anon");
|
||||
|
||||
filterChainDefinitionMap.put("/druid/**", "anon");
|
||||
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
|
||||
filterChainDefinitionMap.put("/swagger**/**", "anon");
|
||||
filterChainDefinitionMap.put("/webjars/**", "anon");
|
||||
filterChainDefinitionMap.put("/v3/**", "anon");
|
||||
|
||||
filterChainDefinitionMap.put("/sys/annountCement/show/**", "anon");
|
||||
|
||||
//积木报表排除
|
||||
filterChainDefinitionMap.put("/jmreport/**", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.js.map", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.css.map", "anon");
|
||||
|
||||
//积木BI大屏和仪表盘排除
|
||||
filterChainDefinitionMap.put("/drag/view", "anon");
|
||||
filterChainDefinitionMap.put("/drag/page/queryById", "anon");
|
||||
filterChainDefinitionMap.put("/drag/page/addVisitsNumber", "anon");
|
||||
filterChainDefinitionMap.put("/drag/page/queryTemplateList", "anon");
|
||||
filterChainDefinitionMap.put("/drag/share/view/**", "anon");
|
||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getAllChartData", "anon");
|
||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getTotalData", "anon");
|
||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getMapDataByCode", "anon");
|
||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getTotalDataByCompId", "anon");
|
||||
filterChainDefinitionMap.put("/drag/mock/json/**", "anon");
|
||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getDictByCodes", "anon");
|
||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/queryAllById", "anon");
|
||||
filterChainDefinitionMap.put("/jimubi/view", "anon");
|
||||
filterChainDefinitionMap.put("/jimubi/share/view/**", "anon");
|
||||
|
||||
//大屏模板例子
|
||||
filterChainDefinitionMap.put("/test/bigScreen/**", "anon");
|
||||
filterChainDefinitionMap.put("/bigscreen/template1/**", "anon");
|
||||
filterChainDefinitionMap.put("/bigscreen/template2/**", "anon");
|
||||
//filterChainDefinitionMap.put("/test/jeecgDemo/rabbitMqClientTest/**", "anon"); //MQ测试
|
||||
//filterChainDefinitionMap.put("/test/jeecgDemo/html", "anon"); //模板页面
|
||||
//filterChainDefinitionMap.put("/test/jeecgDemo/redis/**", "anon"); //redis测试
|
||||
|
||||
//websocket排除
|
||||
filterChainDefinitionMap.put("/websocket/**", "anon");//系统通知和公告
|
||||
filterChainDefinitionMap.put("/newsWebsocket/**", "anon");//CMS模块
|
||||
filterChainDefinitionMap.put("/vxeSocket/**", "anon");//JVxeTable无痕刷新示例
|
||||
//App vue3版本查询版本接口
|
||||
filterChainDefinitionMap.put("/sys/version/app3version", "anon");
|
||||
//仪表盘(按钮通信)
|
||||
filterChainDefinitionMap.put("/dragChannelSocket/**","anon");
|
||||
//App vue3版本查询版本接口
|
||||
filterChainDefinitionMap.put("/sys/version/app3version", "anon");
|
||||
|
||||
//性能监控——安全隐患泄露TOEKN(durid连接池也有)
|
||||
//filterChainDefinitionMap.put("/actuator/**", "anon");
|
||||
//测试模块排除
|
||||
filterChainDefinitionMap.put("/test/seata/**", "anon");
|
||||
|
||||
//错误路径排除
|
||||
filterChainDefinitionMap.put("/error", "anon");
|
||||
// 企业微信证书排除
|
||||
filterChainDefinitionMap.put("/WW_verify*", "anon");
|
||||
|
||||
filterChainDefinitionMap.put("/openapi/call/**", "anon");
|
||||
|
||||
// 添加自己的过滤器并且取名为jwt
|
||||
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
|
||||
//如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
|
||||
Object cloudServer = env.getProperty(CommonConstant.CLOUD_SERVER_KEY);
|
||||
filterMap.put("jwt", new JwtFilter(cloudServer==null));
|
||||
shiroFilterFactoryBean.setFilters(filterMap);
|
||||
// <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
|
||||
filterChainDefinitionMap.put("/**", "jwt");
|
||||
|
||||
// 未授权界面返回JSON
|
||||
shiroFilterFactoryBean.setUnauthorizedUrl("/sys/common/403");
|
||||
shiroFilterFactoryBean.setLoginUrl("/sys/common/403");
|
||||
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
|
||||
return shiroFilterFactoryBean;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* spring过滤装饰器 <br/>
|
||||
* 因为shiro的filter不支持异步请求,导致所有的异步请求都会报错. <br/>
|
||||
* 所以需要用spring的FilterRegistrationBean再代理一下shiro的filter.为他扩展异步支持. <br/>
|
||||
* 后续所有异步的接口都需要再这里增加registration.addUrlPatterns("/xxx/xxx");
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2024/12/3 19:49
|
||||
*/
|
||||
@Bean
|
||||
public FilterRegistrationBean shiroFilterRegistration() {
|
||||
FilterRegistrationBean registration = new FilterRegistrationBean();
|
||||
registration.setFilter(new DelegatingFilterProxy("shiroFilterFactoryBean"));
|
||||
registration.setEnabled(true);
|
||||
// 代码逻辑说明: [issues/7491]运行耗时长,效率慢
|
||||
registration.addUrlPatterns("/test/ai/chat/send");
|
||||
registration.addUrlPatterns("/airag/flow/run");
|
||||
registration.addUrlPatterns("/airag/flow/debug");
|
||||
registration.addUrlPatterns("/airag/chat/send");
|
||||
registration.addUrlPatterns("/airag/app/debug");
|
||||
registration.addUrlPatterns("/airag/app/prompt/generate");
|
||||
registration.addUrlPatterns("/airag/chat/receive/**");
|
||||
// 添加SSE接口的异步支持
|
||||
registration.addUrlPatterns("/airag/extData/evaluator/debug");
|
||||
registration.addUrlPatterns("/drag/onlDragDatasetHead/generateChartSse");
|
||||
registration.addUrlPatterns("/drag/onlDragDatasetHead/updateChartOptSse");
|
||||
registration.addUrlPatterns("/drag/onlDragDatasetHead/generateSqlSse");
|
||||
//支持异步
|
||||
registration.setAsyncSupported(true);
|
||||
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
|
||||
return registration;
|
||||
}
|
||||
|
||||
@Bean("securityManager")
|
||||
public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) {
|
||||
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
|
||||
securityManager.setRealm(myRealm);
|
||||
|
||||
/*
|
||||
* 关闭shiro自带的session,详情见文档
|
||||
* http://shiro.apache.org/session-management.html#SessionManagement-
|
||||
* StatelessApplications%28Sessionless%29
|
||||
*/
|
||||
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
|
||||
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
|
||||
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
|
||||
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
|
||||
securityManager.setSubjectDAO(subjectDAO);
|
||||
//自定义缓存实现,使用redis
|
||||
securityManager.setCacheManager(redisCacheManager());
|
||||
return securityManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下面的代码是添加注解支持
|
||||
* @return
|
||||
*/
|
||||
@Bean
|
||||
@DependsOn("lifecycleBeanPostProcessor")
|
||||
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
|
||||
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
|
||||
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
|
||||
/**
|
||||
* 解决重复代理问题 github#994
|
||||
* 添加前缀判断 不匹配 任何Advisor
|
||||
*/
|
||||
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
|
||||
defaultAdvisorAutoProxyCreator.setAdvisorBeanNamePrefix("_no_advisor");
|
||||
return defaultAdvisorAutoProxyCreator;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
|
||||
return new LifecycleBeanPostProcessor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
|
||||
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
|
||||
advisor.setSecurityManager(securityManager);
|
||||
return advisor;
|
||||
}
|
||||
|
||||
/**
|
||||
* cacheManager 缓存 redis实现
|
||||
* 使用的是shiro-redis开源插件
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public RedisCacheManager redisCacheManager() {
|
||||
log.info("===============(1)创建缓存管理器RedisCacheManager");
|
||||
RedisCacheManager redisCacheManager = new RedisCacheManager();
|
||||
redisCacheManager.setRedisManager(redisManager());
|
||||
//redis中针对不同用户缓存(此处的id需要对应user实体中的id字段,用于唯一标识)
|
||||
redisCacheManager.setPrincipalIdFieldName("id");
|
||||
//用户权限信息缓存时间
|
||||
redisCacheManager.setExpire(200000);
|
||||
return redisCacheManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* RedisConfig在项目starter项目中
|
||||
* jeecg-boot-starter-github\jeecg-boot-common\src\main\java\org\jeecg\common\modules\redis\config\RedisConfig.java
|
||||
*
|
||||
* 配置shiro redisManager
|
||||
* 使用的是shiro-redis开源插件
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean
|
||||
public IRedisManager redisManager() {
|
||||
log.info("===============(2)创建RedisManager,连接Redis..");
|
||||
IRedisManager manager;
|
||||
// sentinel cluster redis(【issues/5569】shiro集成 redis 不支持 sentinel 方式部署的redis集群 #5569)
|
||||
if (Objects.nonNull(redisProperties)
|
||||
&& Objects.nonNull(redisProperties.getSentinel())
|
||||
&& !CollectionUtils.isEmpty(redisProperties.getSentinel().getNodes())) {
|
||||
RedisSentinelManager sentinelManager = new RedisSentinelManager();
|
||||
sentinelManager.setMasterName(redisProperties.getSentinel().getMaster());
|
||||
sentinelManager.setHost(String.join(",", redisProperties.getSentinel().getNodes()));
|
||||
sentinelManager.setPassword(redisProperties.getPassword());
|
||||
sentinelManager.setDatabase(redisProperties.getDatabase());
|
||||
|
||||
return sentinelManager;
|
||||
}
|
||||
|
||||
// redis 单机支持,在集群为空,或者集群无机器时候使用 add by jzyadmin@163.com
|
||||
if (lettuceConnectionFactory.getClusterConfiguration() == null || lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().isEmpty()) {
|
||||
RedisManager redisManager = new RedisManager();
|
||||
redisManager.setHost(lettuceConnectionFactory.getHostName() + ":" + lettuceConnectionFactory.getPort());
|
||||
//(lettuceConnectionFactory.getPort());
|
||||
redisManager.setDatabase(lettuceConnectionFactory.getDatabase());
|
||||
redisManager.setTimeout(0);
|
||||
if (!StringUtils.isEmpty(lettuceConnectionFactory.getPassword())) {
|
||||
redisManager.setPassword(lettuceConnectionFactory.getPassword());
|
||||
}
|
||||
manager = redisManager;
|
||||
}else{
|
||||
// redis集群支持,优先使用集群配置
|
||||
RedisClusterManager redisManager = new RedisClusterManager();
|
||||
Set<HostAndPort> portSet = new HashSet<>();
|
||||
lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().forEach(node -> portSet.add(new HostAndPort(node.getHost() , node.getPort())));
|
||||
//update-begin--Author:scott Date:20210531 for:修改集群模式下未设置redis密码的bug issues/I3QNIC
|
||||
if (oConvertUtils.isNotEmpty(lettuceConnectionFactory.getPassword())) {
|
||||
JedisCluster jedisCluster = new JedisCluster(portSet, 2000, 2000, 5,
|
||||
lettuceConnectionFactory.getPassword(), new GenericObjectPoolConfig());
|
||||
redisManager.setPassword(lettuceConnectionFactory.getPassword());
|
||||
redisManager.setJedisCluster(jedisCluster);
|
||||
} else {
|
||||
JedisCluster jedisCluster = new JedisCluster(portSet);
|
||||
redisManager.setJedisCluster(jedisCluster);
|
||||
}
|
||||
manager = redisManager;
|
||||
}
|
||||
return manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解决 ShiroRequestMappingConfig 获取 requestMappingHandlerMapping Bean 冲突
|
||||
* spring-boot-autoconfigure:3.4.5 和 spring-boot-actuator-autoconfigure:3.4.5
|
||||
*/
|
||||
@Primary
|
||||
@Bean
|
||||
public RequestMappingHandlerMapping overridedRequestMappingHandlerMapping() {
|
||||
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
|
||||
mapping.setUrlPathHelper(new ShiroUrlPathHelper());
|
||||
return mapping;
|
||||
}
|
||||
|
||||
private List<String> rebuildUrl(String[] bases, String[] uris) {
|
||||
List<String> urls = new ArrayList<>();
|
||||
for (String base : bases) {
|
||||
for (String uri : uris) {
|
||||
urls.add(prefix(base)+prefix(uri));
|
||||
}
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
private String prefix(String seg) {
|
||||
return seg.startsWith("/") ? seg : "/"+seg;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,239 +0,0 @@
|
||||
package org.jeecg.config.shiro;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authc.AuthenticationException;
|
||||
import org.apache.shiro.authc.AuthenticationInfo;
|
||||
import org.apache.shiro.authc.AuthenticationToken;
|
||||
import org.apache.shiro.authc.SimpleAuthenticationInfo;
|
||||
import org.apache.shiro.authz.AuthorizationInfo;
|
||||
import org.apache.shiro.authz.SimpleAuthorizationInfo;
|
||||
import org.apache.shiro.realm.AuthorizingRealm;
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
import org.jeecg.common.api.CommonAPI;
|
||||
import org.jeecg.common.config.TenantContext;
|
||||
import org.jeecg.common.constant.CacheConstant;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.system.util.JwtUtil;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.RedisUtil;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.common.util.TokenUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @Description: 用户登录鉴权和获取用户授权
|
||||
* @Author: Scott
|
||||
* @Date: 2019-4-23 8:13
|
||||
* @Version: 1.1
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public class ShiroRealm extends AuthorizingRealm {
|
||||
@Lazy
|
||||
@Resource
|
||||
private CommonAPI commonApi;
|
||||
|
||||
@Lazy
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
/**
|
||||
* 必须重写此方法,不然Shiro会报错
|
||||
*/
|
||||
@Override
|
||||
public boolean supports(AuthenticationToken token) {
|
||||
return token instanceof JwtToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限信息认证(包括角色以及权限)是用户访问controller的时候才进行验证(redis存储的此处权限信息)
|
||||
* 触发检测用户权限时才会调用此方法,例如checkRole,checkPermission
|
||||
*
|
||||
* @param principals 身份信息
|
||||
* @return AuthorizationInfo 权限信息
|
||||
*/
|
||||
@Override
|
||||
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
|
||||
log.debug("===============Shiro权限认证开始============ [ roles、permissions]==========");
|
||||
String username = null;
|
||||
String userId = null;
|
||||
if (principals != null) {
|
||||
LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal();
|
||||
username = sysUser.getUsername();
|
||||
userId = sysUser.getId();
|
||||
}
|
||||
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
|
||||
|
||||
// 设置用户拥有的角色集合,比如“admin,test”
|
||||
Set<String> roleSet = commonApi.queryUserRolesById(userId);
|
||||
//System.out.println(roleSet.toString());
|
||||
info.setRoles(roleSet);
|
||||
|
||||
// 设置用户拥有的权限集合,比如“sys:role:add,sys:user:add”
|
||||
Set<String> permissionSet = commonApi.queryUserAuths(userId);
|
||||
info.addStringPermissions(permissionSet);
|
||||
//System.out.println(permissionSet);
|
||||
log.debug("===============Shiro权限认证成功==============");
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息认证是在用户进行登录的时候进行验证(不存redis)
|
||||
* 也就是说验证用户输入的账号和密码是否正确,错误抛出异常
|
||||
*
|
||||
* @param auth 用户登录的账号密码信息
|
||||
* @return 返回封装了用户信息的 AuthenticationInfo 实例
|
||||
* @throws AuthenticationException
|
||||
*/
|
||||
@Override
|
||||
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
|
||||
log.debug("===============Shiro身份认证开始============doGetAuthenticationInfo==========");
|
||||
String token = (String) auth.getCredentials();
|
||||
if (token == null) {
|
||||
HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
|
||||
log.info("————————身份认证失败——————————IP地址: "+ oConvertUtils.getIpAddrByRequest(req) +",URL:"+req.getRequestURI());
|
||||
throw new AuthenticationException("token为空!");
|
||||
}
|
||||
// 校验token有效性
|
||||
LoginUser loginUser = null;
|
||||
try {
|
||||
loginUser = this.checkUserTokenIsEffect(token);
|
||||
} catch (AuthenticationException e) {
|
||||
log.error("—————校验 check token 失败——————————"+ e.getMessage(), e);
|
||||
// 重新抛出异常,让JwtFilter统一处理,避免返回两次错误响应
|
||||
throw e;
|
||||
}
|
||||
return new SimpleAuthenticationInfo(loginUser, token, getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验token的有效性
|
||||
*
|
||||
* @param token
|
||||
*/
|
||||
public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
|
||||
// 解密获得username,用于和数据库进行对比
|
||||
String username = JwtUtil.getUsername(token);
|
||||
if (username == null) {
|
||||
throw new AuthenticationException("Token非法无效!");
|
||||
}
|
||||
|
||||
// 查询用户信息
|
||||
log.debug("———校验token是否有效————checkUserTokenIsEffect——————— "+ token);
|
||||
LoginUser loginUser = TokenUtils.getLoginUser(username, commonApi, redisUtil);
|
||||
//LoginUser loginUser = commonApi.getUserByName(username);
|
||||
if (loginUser == null) {
|
||||
throw new AuthenticationException("用户不存在!");
|
||||
}
|
||||
// 判断用户状态
|
||||
if (loginUser.getStatus() != 1) {
|
||||
throw new AuthenticationException("账号已被锁定,请联系管理员!");
|
||||
}
|
||||
// 校验token是否超时失效 & 或者账号密码是否错误
|
||||
if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {
|
||||
// 用户登录Token过期提示信息
|
||||
String userLoginTokenErrorMsg = oConvertUtils.getString(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN_ERROR_MSG + token));
|
||||
throw new AuthenticationException(oConvertUtils.isEmpty(userLoginTokenErrorMsg)? CommonConstant.TOKEN_IS_INVALID_MSG: userLoginTokenErrorMsg);
|
||||
}
|
||||
// 代码逻辑说明: 校验用户的tenant_id和前端传过来的是否一致
|
||||
String userTenantIds = loginUser.getRelTenantIds();
|
||||
if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && oConvertUtils.isNotEmpty(userTenantIds)){
|
||||
String contextTenantId = TenantContext.getTenant();
|
||||
log.debug("登录租户:" + contextTenantId);
|
||||
log.debug("用户拥有那些租户:" + userTenantIds);
|
||||
//登录用户无租户,前端header中租户ID值为 0
|
||||
String str ="0";
|
||||
if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){
|
||||
// 代码逻辑说明: /issues/I4O14W 用户租户信息变更判断漏洞
|
||||
String[] arr = userTenantIds.split(",");
|
||||
if(!oConvertUtils.isIn(contextTenantId, arr)){
|
||||
boolean isAuthorization = false;
|
||||
//========================================================================
|
||||
// 查询用户信息(如果租户不匹配从数据库中重新查询一次用户信息)
|
||||
String loginUserKey = CacheConstant.SYS_USERS_CACHE + "::" + username;
|
||||
redisUtil.del(loginUserKey);
|
||||
LoginUser loginUserFromDb = commonApi.getUserByName(username);
|
||||
if (oConvertUtils.isNotEmpty(loginUserFromDb.getRelTenantIds())) {
|
||||
String[] newArray = loginUserFromDb.getRelTenantIds().split(",");
|
||||
if (oConvertUtils.isIn(contextTenantId, newArray)) {
|
||||
isAuthorization = true;
|
||||
}
|
||||
}
|
||||
//========================================================================
|
||||
|
||||
//*********************************************
|
||||
if(!isAuthorization){
|
||||
log.info("租户异常——登录租户:" + contextTenantId);
|
||||
log.info("租户异常——用户拥有租户组:" + userTenantIds);
|
||||
throw new AuthenticationException("登录租户授权变更,请重新登陆!");
|
||||
}
|
||||
//*********************************************
|
||||
}
|
||||
}
|
||||
}
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* JWTToken刷新生命周期 (实现: 用户在线操作不掉线功能)
|
||||
* 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样),缓存有效期设置为Jwt有效时间的2倍
|
||||
* 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
|
||||
* 3、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
|
||||
* 4、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。
|
||||
* 注意: 前端请求Header中设置Authorization保持不变,校验有效性以缓存中的token为准。
|
||||
* 用户过期时间 = Jwt有效时间 * 2。
|
||||
*
|
||||
* @param userName
|
||||
* @param passWord
|
||||
* @return
|
||||
*/
|
||||
public boolean jwtTokenRefresh(String token, String userName, String passWord) {
|
||||
String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
|
||||
if (oConvertUtils.isNotEmpty(cacheToken)) {
|
||||
// 校验token有效性
|
||||
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
|
||||
// 从token中解析客户端类型,保持续期时使用相同的客户端类型
|
||||
String clientType = JwtUtil.getClientType(token);
|
||||
String newAuthorization = JwtUtil.sign(userName, passWord, clientType);
|
||||
// 根据客户端类型设置对应的缓存有效时间
|
||||
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
|
||||
? JwtUtil.APP_EXPIRE_TIME * 2 / 1000
|
||||
: JwtUtil.EXPIRE_TIME * 2 / 1000;
|
||||
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
|
||||
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, expireTime);
|
||||
log.debug("——————————用户在线操作,更新token保证不掉线—————————jwtTokenRefresh——————— "+ token);
|
||||
}
|
||||
// else {
|
||||
// // 设置超时时间
|
||||
// redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
|
||||
// redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
|
||||
// }
|
||||
return true;
|
||||
}
|
||||
|
||||
//redis中不存在此TOEKN,说明token非法返回false
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除当前用户的权限认证缓存
|
||||
*
|
||||
* @param principals 权限信息
|
||||
*/
|
||||
@Override
|
||||
public void clearCache(PrincipalCollection principals) {
|
||||
super.clearCache(principals);
|
||||
// 代码逻辑说明: 【TV360X-1320】分配权限必须退出重新登录才生效,造成很多用户困扰---
|
||||
super.clearCachedAuthorizationInfo(principals);
|
||||
}
|
||||
}
|
||||
@ -1,77 +0,0 @@
|
||||
package org.jeecg.config.shiro.filters;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
|
||||
import org.apache.shiro.web.filter.InvalidRequestFilter;
|
||||
import org.apache.shiro.web.filter.mgt.DefaultFilter;
|
||||
import org.apache.shiro.web.filter.mgt.FilterChainManager;
|
||||
import org.apache.shiro.web.filter.mgt.FilterChainResolver;
|
||||
import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
|
||||
import org.apache.shiro.web.mgt.WebSecurityManager;
|
||||
import org.apache.shiro.web.servlet.AbstractShiroFilter;
|
||||
import org.apache.shiro.mgt.SecurityManager;
|
||||
import org.springframework.beans.factory.BeanInitializationException;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 自定义ShiroFilterFactoryBean解决资源中文路径问题
|
||||
* @author: jeecg-boot
|
||||
*/
|
||||
@Slf4j
|
||||
public class CustomShiroFilterFactoryBean extends ShiroFilterFactoryBean {
|
||||
@Override
|
||||
public Class getObjectType() {
|
||||
return MySpringShiroFilter.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractShiroFilter createInstance() throws Exception {
|
||||
|
||||
SecurityManager securityManager = getSecurityManager();
|
||||
if (securityManager == null) {
|
||||
String msg = "SecurityManager property must be set.";
|
||||
throw new BeanInitializationException(msg);
|
||||
}
|
||||
|
||||
if (!(securityManager instanceof WebSecurityManager)) {
|
||||
String msg = "The security manager does not implement the WebSecurityManager interface.";
|
||||
throw new BeanInitializationException(msg);
|
||||
}
|
||||
|
||||
FilterChainManager manager = createFilterChainManager();
|
||||
//Expose the constructed FilterChainManager by first wrapping it in a
|
||||
// FilterChainResolver implementation. The AbstractShiroFilter implementations
|
||||
// do not know about FilterChainManagers - only resolvers:
|
||||
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
|
||||
chainResolver.setFilterChainManager(manager);
|
||||
|
||||
Map<String, Filter> filterMap = manager.getFilters();
|
||||
Filter invalidRequestFilter = filterMap.get(DefaultFilter.invalidRequest.name());
|
||||
if (invalidRequestFilter instanceof InvalidRequestFilter) {
|
||||
//此处是关键,设置false跳过URL携带中文400,servletPath中文校验bug
|
||||
((InvalidRequestFilter) invalidRequestFilter).setBlockNonAscii(false);
|
||||
}
|
||||
//Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built
|
||||
//FilterChainResolver. It doesn't matter that the instance is an anonymous inner class
|
||||
//here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts
|
||||
//injection of the SecurityManager and FilterChainResolver:
|
||||
return new MySpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
|
||||
}
|
||||
|
||||
private static final class MySpringShiroFilter extends AbstractShiroFilter {
|
||||
protected MySpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {
|
||||
if (webSecurityManager == null) {
|
||||
throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
|
||||
} else {
|
||||
this.setSecurityManager(webSecurityManager);
|
||||
if (resolver != null) {
|
||||
this.setFilterChainResolver(resolver);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,132 +0,0 @@
|
||||
package org.jeecg.config.shiro.filters;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
|
||||
import org.jeecg.common.config.TenantContext;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.system.util.JwtUtil;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.shiro.JwtToken;
|
||||
import org.jeecg.config.shiro.ignore.InMemoryIgnoreAuth;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* @Description: 鉴权登录拦截器
|
||||
* @Author: Scott
|
||||
* @Date: 2018/10/7
|
||||
**/
|
||||
@Slf4j
|
||||
public class JwtFilter extends BasicHttpAuthenticationFilter {
|
||||
|
||||
/**
|
||||
* 默认开启跨域设置(使用单体)
|
||||
* 微服务情况下,此属性设置为false
|
||||
*/
|
||||
private boolean allowOrigin = true;
|
||||
|
||||
public JwtFilter(){}
|
||||
public JwtFilter(boolean allowOrigin){
|
||||
this.allowOrigin = allowOrigin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行登录认证
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @param mappedValue
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
|
||||
try {
|
||||
// 判断当前路径是不是注解了@IngoreAuth路径,如果是,则放开验证
|
||||
if (InMemoryIgnoreAuth.contains(((HttpServletRequest) request).getServletPath())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
executeLogin(request, response);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
// 使用异常中的具体错误信息,保留"不允许同一账号多地同时登录"等具体提示
|
||||
String errorMsg = e.getMessage();
|
||||
if (oConvertUtils.isEmpty(errorMsg)) {
|
||||
errorMsg = CommonConstant.TOKEN_IS_INVALID_MSG;
|
||||
}
|
||||
JwtUtil.responseError((HttpServletResponse)response, 401, errorMsg);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
|
||||
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
||||
String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);
|
||||
// 代码逻辑说明: JT-355 OA聊天添加token验证,获取token参数
|
||||
if (oConvertUtils.isEmpty(token)) {
|
||||
token = httpServletRequest.getParameter("token");
|
||||
}
|
||||
|
||||
JwtToken jwtToken = new JwtToken(token);
|
||||
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
|
||||
getSubject(request, response).login(jwtToken);
|
||||
// 如果没有抛出异常则代表登入成功,返回true
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对跨域提供支持
|
||||
*/
|
||||
@Override
|
||||
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
|
||||
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
||||
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
||||
if(allowOrigin){
|
||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, httpServletRequest.getHeader(HttpHeaders.ORIGIN));
|
||||
// 允许客户端请求方法
|
||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET,POST,OPTIONS,PUT,DELETE");
|
||||
// 允许客户端提交的Header
|
||||
String requestHeaders = httpServletRequest.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
|
||||
if (StringUtils.isNotEmpty(requestHeaders)) {
|
||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders);
|
||||
}
|
||||
// 允许客户端携带凭证信息(是否允许发送Cookie)
|
||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
|
||||
}
|
||||
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
|
||||
if (RequestMethod.OPTIONS.name().equalsIgnoreCase(httpServletRequest.getMethod())) {
|
||||
httpServletResponse.setStatus(HttpStatus.OK.value());
|
||||
return false;
|
||||
}
|
||||
// 代码逻辑说明: 多租户用到
|
||||
String tenantId = httpServletRequest.getHeader(CommonConstant.TENANT_ID);
|
||||
TenantContext.setTenant(tenantId);
|
||||
|
||||
return super.preHandle(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* JwtFilter中ThreadLocal需要及时清除 #3634
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @param exception
|
||||
* @throws Exception
|
||||
*/
|
||||
@Override
|
||||
public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception {
|
||||
//log.info("------清空线程中多租户的ID={}------",TenantContext.getTenant());
|
||||
TenantContext.clear();
|
||||
}
|
||||
}
|
||||
@ -1,67 +0,0 @@
|
||||
package org.jeecg.config.shiro.filters;
|
||||
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.web.filter.AccessControlFilter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* @Author Scott
|
||||
* @create 2019-02-01 15:56
|
||||
* @desc 鉴权请求URL访问权限拦截器
|
||||
*/
|
||||
@Slf4j
|
||||
public class ResourceCheckFilter extends AccessControlFilter {
|
||||
|
||||
private String errorUrl;
|
||||
|
||||
public String getErrorUrl() {
|
||||
return errorUrl;
|
||||
}
|
||||
|
||||
public void setErrorUrl(String errorUrl) {
|
||||
this.errorUrl = errorUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 表示是否允许访问 ,如果允许访问返回true,否则false;
|
||||
*
|
||||
* @param servletRequest
|
||||
* @param servletResponse
|
||||
* @param o 表示写在拦截器中括号里面的字符串 mappedValue 就是 [urls] 配置中拦截器参数部分
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
@Override
|
||||
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
|
||||
Subject subject = getSubject(servletRequest, servletResponse);
|
||||
String url = getPathWithinApplication(servletRequest);
|
||||
log.info("当前用户正在访问的 url => " + url);
|
||||
return subject.isPermitted(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* onAccessDenied:表示当访问拒绝时是否已经处理了; 如果返回 true 表示需要继续处理; 如果返回 false
|
||||
* 表示该拦截器实例已经处理了,将直接返回即可。
|
||||
*
|
||||
* @param servletRequest
|
||||
* @param servletResponse
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
@Override
|
||||
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
|
||||
log.info("当 isAccessAllowed 返回 false 的时候,才会执行 method onAccessDenied ");
|
||||
|
||||
HttpServletRequest request = (HttpServletRequest) servletRequest;
|
||||
HttpServletResponse response = (HttpServletResponse) servletResponse;
|
||||
response.sendRedirect(request.getContextPath() + this.errorUrl);
|
||||
|
||||
// 返回 false 表示已经处理,例如页面跳转啥的,表示不在走以下的拦截器了(如果还有配置的话)
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
package org.jeecg.config.sign.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 签名校验注解
|
||||
* 用于方法级别的签名验证,功能等同于yml中的jeecg.signUrls配置
|
||||
* 参考DragSignatureAspect的设计思路,使用AOP切面实现
|
||||
*
|
||||
* @author GitHub Copilot
|
||||
* @since 2025-12-15
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface SignatureCheck {
|
||||
|
||||
/**
|
||||
* 是否启用签名校验
|
||||
* @return true-启用(默认), false-禁用
|
||||
*/
|
||||
boolean enabled() default true;
|
||||
|
||||
/**
|
||||
* 签名校验失败时的错误消息
|
||||
* @return 错误消息
|
||||
*/
|
||||
String errorMessage() default "Sign签名校验失败!";
|
||||
|
||||
}
|
||||
@ -1,112 +0,0 @@
|
||||
package org.jeecg.config.sign.aspect;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.jeecg.config.sign.annotation.SignatureCheck;
|
||||
import org.jeecg.config.sign.interceptor.SignAuthInterceptor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 基于AOP的签名验证切面
|
||||
* 复用SignAuthInterceptor的成熟签名验证逻辑
|
||||
*
|
||||
* @author GitHub Copilot
|
||||
* @since 2025-12-15
|
||||
*/
|
||||
@Aspect
|
||||
@Slf4j
|
||||
@Component("signatureCheckAspect")
|
||||
public class SignatureCheckAspect {
|
||||
|
||||
/**
|
||||
* 复用SignAuthInterceptor的签名验证逻辑
|
||||
*/
|
||||
private final SignAuthInterceptor signAuthInterceptor = new SignAuthInterceptor();
|
||||
|
||||
/**
|
||||
* 验签切点:拦截所有标记了@SignatureCheck注解的方法
|
||||
*/
|
||||
@Pointcut("@annotation(org.jeecg.config.sign.annotation.SignatureCheck)")
|
||||
private void signatureCheckPointCut() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始验签
|
||||
*/
|
||||
@Before("signatureCheckPointCut()")
|
||||
public void doSignatureValidation(JoinPoint point) throws Exception {
|
||||
// 获取方法上的注解
|
||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
SignatureCheck signatureCheck = method.getAnnotation(SignatureCheck.class);
|
||||
|
||||
log.info("AOP签名验证: {}.{}", method.getDeclaringClass().getSimpleName(), method.getName());
|
||||
|
||||
// 如果注解被禁用,直接返回
|
||||
if (!signatureCheck.enabled()) {
|
||||
log.info("签名验证已禁用,跳过");
|
||||
return;
|
||||
}
|
||||
|
||||
// update-begin---author:sjlei---date:20260115 for: 查找带有@RequestBody注解的参数,解决签名校验时读取请求体为空的问题
|
||||
Object bodyParam = null;
|
||||
Object[] args = point.getArgs();
|
||||
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
Object arg = args[i];
|
||||
Annotation[] annotations = parameterAnnotations[i];
|
||||
boolean hasRequestBodyAnnotation = Arrays.stream(annotations).anyMatch(annotation -> annotation.annotationType().equals(RequestBody.class));
|
||||
if (hasRequestBodyAnnotation) {
|
||||
// 捕获携带@RequestBody注解的参数,供签名校验使用
|
||||
bodyParam = arg;
|
||||
}
|
||||
}
|
||||
// update-end-----author:sjlei---date:20260115 for: 查找带有@RequestBody注解的参数,解决签名校验时读取请求体为空的问题
|
||||
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
log.error("无法获取请求上下文");
|
||||
throw new IllegalArgumentException("无法获取请求上下文");
|
||||
}
|
||||
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
log.info("X-SIGN: {}, X-TIMESTAMP: {}", request.getHeader("X-SIGN"), request.getHeader("X-TIMESTAMP"));
|
||||
|
||||
try {
|
||||
// 直接调用SignAuthInterceptor的验证逻辑
|
||||
signAuthInterceptor.validateSignature(request, bodyParam);
|
||||
log.info("AOP签名验证通过");
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
// 使用注解中配置的错误消息,或者保留原始错误消息
|
||||
String errorMessage = signatureCheck.errorMessage();
|
||||
log.error("AOP签名验证失败: {}", e.getMessage());
|
||||
|
||||
if ("Sign签名校验失败!".equals(errorMessage)) {
|
||||
// 如果是默认错误消息,使用原始的详细错误信息
|
||||
throw e;
|
||||
} else {
|
||||
// 如果是自定义错误消息,使用自定义消息
|
||||
throw new IllegalArgumentException(errorMessage, e);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 包装其他异常
|
||||
String errorMessage = signatureCheck.errorMessage();
|
||||
log.error("AOP签名验证异常: {}", e.getMessage());
|
||||
throw new IllegalArgumentException(errorMessage, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
package org.jeecg.config.sign.interceptor;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.common.constant.TenantConstant;
|
||||
import org.jeecg.common.util.PathMatcherUtil;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.config.filter.RequestBodyReserveFilter;
|
||||
@ -65,8 +64,6 @@ public class SignAuthConfiguration implements WebMvcConfigurer {
|
||||
//------------------------------------------------------------
|
||||
// 建议此处只添加post请求地址而不是所有的都需要走过滤器
|
||||
registration.addUrlPatterns(signUrlsArray);
|
||||
// 增加注解签名请求
|
||||
registration.addUrlPatterns(TenantConstant.SIGNATURE_CHECK_POST_URL);
|
||||
return registration;
|
||||
}
|
||||
|
||||
|
||||
@ -33,104 +33,63 @@ public class SignAuthInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
log.info("签名拦截器 Interceptor request URI = " + request.getRequestURI());
|
||||
log.debug("Sign Interceptor request URI = " + request.getRequestURI());
|
||||
HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
|
||||
//获取全部参数(包括URL和body上的)
|
||||
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper);
|
||||
//对参数进行签名验证
|
||||
String headerSign = request.getHeader(CommonConstant.X_SIGN);
|
||||
String xTimestamp = request.getHeader(CommonConstant.X_TIMESTAMP);
|
||||
|
||||
try {
|
||||
// 调用验证逻辑
|
||||
validateSignature(request);
|
||||
return true;
|
||||
} catch (IllegalArgumentException e) {
|
||||
// 验证失败,返回错误响应
|
||||
log.error("Sign 签名校验失败!{}", e.getMessage());
|
||||
if(oConvertUtils.isEmpty(xTimestamp)){
|
||||
Result<?> result = Result.error("Sign签名校验失败,时间戳为空!");
|
||||
log.error("Sign 签名校验失败!Header xTimestamp 为空");
|
||||
//校验失败返回前端
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
response.setContentType("application/json; charset=utf-8");
|
||||
PrintWriter out = response.getWriter();
|
||||
Result<?> result = Result.error(e.getMessage());
|
||||
out.print(JSON.toJSON(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
//客户端时间
|
||||
Long clientTimestamp = Long.parseLong(xTimestamp);
|
||||
|
||||
int length = 14;
|
||||
int length1000 = 1000;
|
||||
//1.校验签名时间(兼容X_TIMESTAMP的新老格式)
|
||||
if (xTimestamp.length() == length) {
|
||||
//a. X_TIMESTAMP格式是 yyyyMMddHHmmss (例子:20220308152143)
|
||||
if ((DateUtils.getCurrentTimestamp() - clientTimestamp) > MAX_EXPIRE) {
|
||||
log.error("签名验证失败:X-TIMESTAMP已过期,注意系统时间和服务器时间是否有误差!");
|
||||
throw new IllegalArgumentException("签名验证失败:X-TIMESTAMP已过期");
|
||||
}
|
||||
} else {
|
||||
//b. X_TIMESTAMP格式是 时间戳 (例子:1646552406000)
|
||||
if ((System.currentTimeMillis() - clientTimestamp) > (MAX_EXPIRE * length1000)) {
|
||||
log.error("签名验证失败:X-TIMESTAMP已过期,注意系统时间和服务器时间是否有误差!");
|
||||
throw new IllegalArgumentException("签名验证失败:X-TIMESTAMP已过期");
|
||||
}
|
||||
}
|
||||
|
||||
//2.校验签名
|
||||
boolean isSigned = SignUtil.verifySign(allParams,headerSign);
|
||||
|
||||
if (isSigned) {
|
||||
log.debug("Sign 签名通过!Header Sign : {}",headerSign);
|
||||
return true;
|
||||
} else {
|
||||
log.debug("sign allParams: {}", allParams);
|
||||
log.error("request URI = " + request.getRequestURI());
|
||||
log.error("Sign 签名校验失败!Header Sign : {}",headerSign);
|
||||
//校验失败返回前端
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
response.setContentType("application/json; charset=utf-8");
|
||||
PrintWriter out = response.getWriter();
|
||||
Result<?> result = Result.error("Sign签名校验失败!");
|
||||
out.print(JSON.toJSON(result));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 签名验证核心逻辑
|
||||
* 提取出来供AOP切面复用
|
||||
* @param request HTTP请求
|
||||
* @throws IllegalArgumentException 验证失败时抛出异常
|
||||
*/
|
||||
public void validateSignature(HttpServletRequest request) throws IllegalArgumentException {
|
||||
validateSignature(request, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 签名验证核心逻辑
|
||||
* 提取出来供AOP切面复用
|
||||
* @param request HTTP请求
|
||||
* @throws IllegalArgumentException 验证失败时抛出异常
|
||||
*/
|
||||
public void validateSignature(HttpServletRequest request, Object bodyParam) throws IllegalArgumentException {
|
||||
try {
|
||||
log.debug("开始签名验证: {} {}", request.getMethod(), request.getRequestURI());
|
||||
|
||||
HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
|
||||
//获取全部参数(包括URL和body上的)
|
||||
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper, bodyParam);
|
||||
log.debug("提取参数: {}", allParams);
|
||||
|
||||
//对参数进行签名验证
|
||||
String headerSign = request.getHeader(CommonConstant.X_SIGN);
|
||||
String xTimestamp = request.getHeader(CommonConstant.X_TIMESTAMP);
|
||||
|
||||
if(oConvertUtils.isEmpty(xTimestamp)){
|
||||
log.error("Sign签名校验失败,时间戳为空!");
|
||||
throw new IllegalArgumentException("Sign签名校验失败,请求参数不完整!");
|
||||
}
|
||||
|
||||
//客户端时间
|
||||
Long clientTimestamp = Long.parseLong(xTimestamp);
|
||||
|
||||
int length = 14;
|
||||
int length1000 = 1000;
|
||||
//1.校验签名时间(兼容X_TIMESTAMP的新老格式)
|
||||
if (xTimestamp.length() == length) {
|
||||
//a. X_TIMESTAMP格式是 yyyyMMddHHmmss (例子:20220308152143)
|
||||
long currentTimestamp = DateUtils.getCurrentTimestamp();
|
||||
long timeDiff = currentTimestamp - clientTimestamp;
|
||||
log.debug("时间戳验证(yyyyMMddHHmmss): 时间差{}秒", timeDiff);
|
||||
|
||||
if (timeDiff > MAX_EXPIRE) {
|
||||
log.error("时间戳已过期: {}秒 > {}秒", timeDiff, MAX_EXPIRE);
|
||||
throw new IllegalArgumentException("签名验证失败,请求时效性验证失败!");
|
||||
}
|
||||
} else {
|
||||
//b. X_TIMESTAMP格式是 时间戳 (例子:1646552406000)
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long timeDiff = currentTime - clientTimestamp;
|
||||
long maxExpireMs = MAX_EXPIRE * length1000;
|
||||
log.debug("时间戳验证(Unix): 时间差{}ms", timeDiff);
|
||||
|
||||
if (timeDiff > maxExpireMs) {
|
||||
log.error("时间戳已过期: {}ms > {}ms", timeDiff, maxExpireMs);
|
||||
throw new IllegalArgumentException("签名验证失败,请求时效性验证失败!");
|
||||
}
|
||||
}
|
||||
|
||||
//2.校验签名
|
||||
boolean isSigned = SignUtil.verifySign(allParams,headerSign);
|
||||
|
||||
if (isSigned) {
|
||||
log.debug("签名验证通过");
|
||||
} else {
|
||||
log.error("签名验证失败, 参数: {}", allParams);
|
||||
throw new IllegalArgumentException("Sign签名校验失败!");
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
// 重新抛出签名验证异常
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
// 包装其他异常(如IOException)
|
||||
log.error("签名验证异常: {}", e.getMessage());
|
||||
throw new IllegalArgumentException("Sign签名校验失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -35,7 +35,7 @@ public class HttpUtils {
|
||||
* @date 20210621
|
||||
* @param request
|
||||
*/
|
||||
public static SortedMap<String, String> getAllParams(HttpServletRequest request, Object bodyParam) throws IOException {
|
||||
public static SortedMap<String, String> getAllParams(HttpServletRequest request) throws IOException {
|
||||
|
||||
SortedMap<String, String> result = new TreeMap<>();
|
||||
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
|
||||
@ -65,13 +65,7 @@ public class HttpUtils {
|
||||
Map<String, String> allRequestParam = new HashMap<>(16);
|
||||
// get请求不需要拿body参数
|
||||
if (!HttpMethod.GET.name().equals(request.getMethod())) {
|
||||
if (bodyParam != null) {
|
||||
// update-begin---author:sjlei---date:20260115 for: 解决签名校验时读取请求体为空的问题
|
||||
allRequestParam = JSONObject.parseObject(JSONObject.toJSONString(bodyParam), Map.class);
|
||||
// update-end-----author:sjlei---date:20260115 for: 解决签名校验时读取请求体为空的问题
|
||||
} else {
|
||||
allRequestParam = getAllRequestParam(request);
|
||||
}
|
||||
allRequestParam = getAllRequestParam(request);
|
||||
}
|
||||
// 将URL的参数和body参数进行合并
|
||||
if (allRequestParam != null) {
|
||||
|
||||
@ -11,12 +11,7 @@ import org.springframework.util.DigestUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* 签名工具类
|
||||
@ -54,7 +49,12 @@ public class SignUtil {
|
||||
String paramsJsonStr = JSONObject.toJSONString(params);
|
||||
log.debug("Param paramsJsonStr : {}", paramsJsonStr);
|
||||
//设置签名秘钥
|
||||
String signatureSecret = SignUtil.getSignatureSecret();
|
||||
JeecgBaseConfig jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
|
||||
String signatureSecret = jeecgBaseConfig.getSignatureSecret();
|
||||
String curlyBracket = SymbolConstant.DOLLAR + SymbolConstant.LEFT_CURLY_BRACKET;
|
||||
if(oConvertUtils.isEmpty(signatureSecret) || signatureSecret.contains(curlyBracket)){
|
||||
throw new JeecgBootException("签名密钥 ${jeecg.signatureSecret} 缺少配置 !!");
|
||||
}
|
||||
try {
|
||||
//【issues/I484RW】2.4.6部署后,下拉搜索框提示“sign签名检验失败”
|
||||
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes("UTF-8")).toUpperCase();
|
||||
@ -63,129 +63,4 @@ public class SignUtil {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过前端签名算法生成签名
|
||||
*
|
||||
* @param url 请求的完整URL(包含查询参数)
|
||||
* @param requestParams 使用 @RequestParam 获取的参数集合
|
||||
* @param requestBodyParams 使用 @RequestBody 获取的参数集合
|
||||
* @return 计算得到的签名(大写MD5),若参数不足返回 null
|
||||
*/
|
||||
public static String generateRequestSign(String url, Map<String, Object> requestParams, Map<String, Object> requestBodyParams) {
|
||||
if (oConvertUtils.isEmpty(url)) {
|
||||
return null;
|
||||
}
|
||||
// 解析URL上的查询参数与路径变量
|
||||
Map<String, String> urlParams = parseQueryString(url);
|
||||
// 合并URL参数与@RequestParam参数,确保数值和布尔类型转换为字符串
|
||||
Map<String, String> mergedParams = mergeObject(urlParams, requestParams);
|
||||
// 按需合并@RequestBody参数
|
||||
if (requestBodyParams != null && !requestBodyParams.isEmpty()) {
|
||||
mergedParams = mergeObject(mergedParams, requestBodyParams);
|
||||
}
|
||||
// 按键名升序排序,保持与前端一致的签名顺序
|
||||
SortedMap<String, String> sortedParams = new TreeMap<>(mergedParams);
|
||||
// 去除时间戳字段,避免参与签名
|
||||
sortedParams.remove("_t");
|
||||
// 序列化为JSON字符串
|
||||
String paramsJsonStr = JSONObject.toJSONString(sortedParams);
|
||||
// 读取签名秘钥
|
||||
String signatureSecret = getSignatureSecret();
|
||||
// 计算MD5摘要并转大写
|
||||
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes(StandardCharsets.UTF_8)).toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析URL中的查询参数,并处理末尾逗号分隔的路径变量片段。
|
||||
*
|
||||
* @param url 请求的完整URL
|
||||
* @return 解析后的参数映射,数值与布尔类型均转换为字符串
|
||||
*/
|
||||
private static Map<String, String> parseQueryString(String url) {
|
||||
Map<String, String> result = new HashMap<>(16);
|
||||
int fragmentIndex = url.indexOf('#');
|
||||
if (fragmentIndex >= 0) {
|
||||
url = url.substring(0, fragmentIndex);
|
||||
}
|
||||
int questionIndex = url.indexOf('?');
|
||||
String paramString = null;
|
||||
if (questionIndex >= 0 && questionIndex < url.length() - 1) {
|
||||
paramString = url.substring(questionIndex + 1);
|
||||
}
|
||||
// 处理路径变量末尾以逗号分隔的段,例如 /sys/dict/getDictItems/sys_user,realname,username
|
||||
int lastSlashIndex = url.lastIndexOf(SymbolConstant.SINGLE_SLASH);
|
||||
if (lastSlashIndex >= 0 && lastSlashIndex < url.length() - 1) {
|
||||
String lastPathVariable = url.substring(lastSlashIndex + 1);
|
||||
int qIndexInPath = lastPathVariable.indexOf('?');
|
||||
if (qIndexInPath >= 0) {
|
||||
lastPathVariable = lastPathVariable.substring(0, qIndexInPath);
|
||||
}
|
||||
if (lastPathVariable.contains(SymbolConstant.COMMA)) {
|
||||
String decodedPathVariable = URLDecoder.decode(lastPathVariable, StandardCharsets.UTF_8);
|
||||
result.put(X_PATH_VARIABLE, decodedPathVariable);
|
||||
}
|
||||
}
|
||||
if (oConvertUtils.isNotEmpty(paramString)) {
|
||||
String[] pairs = paramString.split(SymbolConstant.AND);
|
||||
for (String pair : pairs) {
|
||||
int equalIndex = pair.indexOf('=');
|
||||
if (equalIndex > 0 && equalIndex < pair.length() - 1) {
|
||||
String key = pair.substring(0, equalIndex);
|
||||
String value = pair.substring(equalIndex + 1);
|
||||
// 解码并统一类型为字符串
|
||||
String decodedKey = URLDecoder.decode(key, StandardCharsets.UTF_8);
|
||||
String decodedValue = URLDecoder.decode(value, StandardCharsets.UTF_8);
|
||||
result.put(decodedKey, decodedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并两个参数映射,并保证数值与布尔类型统一转为字符串。
|
||||
*
|
||||
* @param target 初始参数映射
|
||||
* @param source 待合并的参数映射
|
||||
* @return 合并后的新映射
|
||||
*/
|
||||
private static Map<String, String> mergeObject(Map<String, String> target, Map<String, Object> source) {
|
||||
Map<String, String> merged = new HashMap<>(16);
|
||||
if (target != null && !target.isEmpty()) {
|
||||
merged.putAll(target);
|
||||
}
|
||||
if (source != null && !source.isEmpty()) {
|
||||
for (Map.Entry<String, Object> entry : source.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
if (value instanceof Number) {
|
||||
// 数值类型转字符串,保持前后端一致
|
||||
merged.put(key, String.valueOf(value));
|
||||
} else if (value instanceof Boolean) {
|
||||
// 布尔类型转字符串,保持前后端一致
|
||||
merged.put(key, String.valueOf(value));
|
||||
} else if (value != null) {
|
||||
merged.put(key, String.valueOf(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取并校验签名秘钥配置。
|
||||
*
|
||||
* @return 有效的签名秘钥
|
||||
*/
|
||||
private static String getSignatureSecret() {
|
||||
JeecgBaseConfig jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
|
||||
String signatureSecret = jeecgBaseConfig.getSignatureSecret();
|
||||
String curlyBracket = SymbolConstant.DOLLAR + SymbolConstant.LEFT_CURLY_BRACKET;
|
||||
if (oConvertUtils.isEmpty(signatureSecret) || signatureSecret.contains(curlyBracket)) {
|
||||
throw new JeecgBootException("签名密钥 ${jeecg.signatureSecret} 缺少配置 !!");
|
||||
}
|
||||
return signatureSecret;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -19,10 +19,6 @@ public class Firewall {
|
||||
* 低代码模式(dev:开发模式,prod:发布模式——关闭所有在线开发配置能力)
|
||||
*/
|
||||
private String lowCodeMode;
|
||||
/**
|
||||
* 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
|
||||
*/
|
||||
private Boolean isConcurrent = true;
|
||||
/**
|
||||
* 是否开启默认密码登录提醒(true 登录后提示必须修改默认密码)
|
||||
*/
|
||||
@ -78,12 +74,4 @@ public class Firewall {
|
||||
public void setDisableSelectAll(Boolean disableSelectAll) {
|
||||
this.disableSelectAll = disableSelectAll;
|
||||
}
|
||||
|
||||
public Boolean getIsConcurrent() {
|
||||
return isConcurrent;
|
||||
}
|
||||
|
||||
public void setIsConcurrent(Boolean isConcurrent) {
|
||||
this.isConcurrent = isConcurrent;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ package org.jeecg.modules.base.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.jeecg.common.util.LoginUserUtils;
|
||||
import org.jeecg.common.api.dto.LogDTO;
|
||||
import org.jeecg.common.constant.enums.ClientTerminalTypeEnum;
|
||||
import org.jeecg.common.util.BrowserUtils;
|
||||
@ -74,7 +74,7 @@ public class BaseCommonServiceImpl implements BaseCommonService {
|
||||
//获取登录用户信息
|
||||
if(user==null){
|
||||
try {
|
||||
user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
user = LoginUserUtils.getSessionUser();
|
||||
} catch (Exception e) {
|
||||
//e.printStackTrace();
|
||||
}
|
||||
|
||||
@ -1,429 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
MCP Stdio 工具 - 修复编码问题
|
||||
确保所有输出都使用UTF-8编码
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from typing import Dict, Any
|
||||
import logging
|
||||
|
||||
# 强制使用UTF-8编码
|
||||
if sys.platform == "win32":
|
||||
# Windows需要特殊处理
|
||||
import io
|
||||
|
||||
sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||
else:
|
||||
# Unix-like系统
|
||||
sys.stdin.reconfigure(encoding='utf-8')
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
sys.stderr.reconfigure(encoding='utf-8')
|
||||
|
||||
# 设置环境变量
|
||||
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
||||
os.environ['PYTHONUTF8'] = '1'
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
encoding='utf-8'
|
||||
)
|
||||
logger = logging.getLogger("mcp-tool")
|
||||
|
||||
|
||||
class FixedMCPServer:
|
||||
"""修复编码问题的MCP服务器"""
|
||||
|
||||
def __init__(self):
|
||||
self.tools = {}
|
||||
self.initialize_tools()
|
||||
|
||||
def initialize_tools(self):
|
||||
"""初始化工具集"""
|
||||
|
||||
# 获取时间
|
||||
self.tools["get_time"] = {
|
||||
"name": "get_time",
|
||||
"description": "获取当前时间",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"format": {
|
||||
"type": "string",
|
||||
"description": "时间格式",
|
||||
"enum": ["iso", "timestamp", "human", "chinese"],
|
||||
"default": "iso"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 文本处理工具
|
||||
self.tools["text_process"] = {
|
||||
"name": "text_process",
|
||||
"description": "文本处理工具",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "输入文本"
|
||||
},
|
||||
"operation": {
|
||||
"type": "string",
|
||||
"description": "操作类型",
|
||||
"enum": ["length", "upper", "lower", "reverse", "count_words"],
|
||||
"default": "length"
|
||||
}
|
||||
},
|
||||
"required": ["text"]
|
||||
}
|
||||
}
|
||||
|
||||
# 数据格式工具
|
||||
self.tools["format_data"] = {
|
||||
"name": "format_data",
|
||||
"description": "格式化数据",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "string",
|
||||
"description": "原始数据"
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"description": "格式类型",
|
||||
"enum": ["json", "yaml", "xml"],
|
||||
"default": "json"
|
||||
}
|
||||
},
|
||||
"required": ["data"]
|
||||
}
|
||||
}
|
||||
|
||||
def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""处理请求"""
|
||||
try:
|
||||
method = request.get("method")
|
||||
params = request.get("params", {})
|
||||
|
||||
if method == "tools/list":
|
||||
return self.handle_tools_list()
|
||||
elif method == "tools/call":
|
||||
return self.handle_tool_call(params)
|
||||
elif method == "ping":
|
||||
return {"result": "pong"}
|
||||
else:
|
||||
return self.create_error_response(
|
||||
code=-32601,
|
||||
message="Method not found"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling request: {e}")
|
||||
return self.create_error_response(
|
||||
code=-32603,
|
||||
message=f"Internal error: {str(e)}"
|
||||
)
|
||||
|
||||
def handle_tools_list(self) -> Dict[str, Any]:
|
||||
"""列出所有工具 - 确保返回标准JSON"""
|
||||
return {
|
||||
"result": {
|
||||
"tools": list(self.tools.values())
|
||||
}
|
||||
}
|
||||
|
||||
def handle_tool_call(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""调用工具 - 修复响应格式"""
|
||||
name = params.get("name")
|
||||
arguments = params.get("arguments", {})
|
||||
|
||||
if name not in self.tools:
|
||||
return self.create_error_response(
|
||||
code=-32602,
|
||||
message=f"Tool '{name}' not found"
|
||||
)
|
||||
|
||||
try:
|
||||
if name == "get_time":
|
||||
result = self.execute_get_time(arguments)
|
||||
elif name == "text_process":
|
||||
result = self.execute_text_process(arguments)
|
||||
elif name == "format_data":
|
||||
result = self.execute_format_data(arguments)
|
||||
else:
|
||||
return self.create_error_response(
|
||||
code=-32602,
|
||||
message="Tool not implemented"
|
||||
)
|
||||
|
||||
# 确保返回正确的MCP响应格式
|
||||
return self.create_success_response(result)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Tool execution error: {e}")
|
||||
return self.create_error_response(
|
||||
code=-32603,
|
||||
message=f"Tool execution failed: {str(e)}"
|
||||
)
|
||||
|
||||
def execute_get_time(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""获取时间 - 支持中文"""
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
format_type = args.get("format", "iso")
|
||||
now = datetime.now()
|
||||
|
||||
if format_type == "iso":
|
||||
result = now.isoformat()
|
||||
elif format_type == "timestamp":
|
||||
result = now.timestamp()
|
||||
elif format_type == "human":
|
||||
result = now.strftime("%Y-%m-%d %H:%M:%S")
|
||||
elif format_type == "chinese":
|
||||
result = now.strftime("%Y年%m月%d日 %H时%M分%S秒")
|
||||
else:
|
||||
result = now.isoformat()
|
||||
logger.info(f"当前系统时间:{result}")
|
||||
return {
|
||||
"status": "success",
|
||||
"format": format_type,
|
||||
"time": result,
|
||||
"timestamp": now.timestamp(),
|
||||
"date": now.strftime("%Y-%m-%d"),
|
||||
"time_12h": now.strftime("%I:%M:%S %p")
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def execute_text_process(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""文本处理"""
|
||||
try:
|
||||
text = args.get("text", "")
|
||||
operation = args.get("operation", "length")
|
||||
|
||||
if operation == "length":
|
||||
result = len(text)
|
||||
result_str = f"文本长度: {result} 个字符"
|
||||
elif operation == "upper":
|
||||
result = text.upper()
|
||||
result_str = f"大写: {result}"
|
||||
elif operation == "lower":
|
||||
result = text.lower()
|
||||
result_str = f"小写: {result}"
|
||||
elif operation == "reverse":
|
||||
result = text[::-1]
|
||||
result_str = f"反转: {result}"
|
||||
elif operation == "count_words":
|
||||
words = len(text.split())
|
||||
result = words
|
||||
result_str = f"单词数: {words}"
|
||||
else:
|
||||
raise ValueError(f"未知操作: {operation}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"operation": operation,
|
||||
"original_text": text,
|
||||
"result": result,
|
||||
"result_str": result_str,
|
||||
"text_length": len(text)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e),
|
||||
"operation": args.get("operation", "")
|
||||
}
|
||||
|
||||
def execute_format_data(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""格式化数据"""
|
||||
try:
|
||||
data_str = args.get("data", "")
|
||||
format_type = args.get("format", "json")
|
||||
|
||||
# 尝试解析为JSON
|
||||
try:
|
||||
data = json.loads(data_str)
|
||||
is_json = True
|
||||
except:
|
||||
data = data_str
|
||||
is_json = False
|
||||
|
||||
if format_type == "json":
|
||||
if is_json:
|
||||
result = json.dumps(data, ensure_ascii=False, indent=2)
|
||||
else:
|
||||
# 如果不是JSON,包装成JSON
|
||||
result = json.dumps({"text": data}, ensure_ascii=False, indent=2)
|
||||
elif format_type == "yaml":
|
||||
import yaml
|
||||
result = yaml.dump(data, allow_unicode=True, default_flow_style=False)
|
||||
elif format_type == "xml":
|
||||
# 简单的XML格式化
|
||||
if isinstance(data, dict):
|
||||
result = "<data>"
|
||||
for k, v in data.items():
|
||||
result += f"\n <{k}>{v}</{k}>"
|
||||
result += "\n</data>"
|
||||
else:
|
||||
result = f"<text>{data}</text>"
|
||||
else:
|
||||
result = str(data)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"format": format_type,
|
||||
"original": data_str,
|
||||
"formatted": result,
|
||||
"length": len(result)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e),
|
||||
"format": args.get("format", "")
|
||||
}
|
||||
|
||||
def create_success_response(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""创建成功响应 - 确保符合MCP规范"""
|
||||
# 将数据转换为JSON字符串作为文本内容
|
||||
content_text = json.dumps(data, ensure_ascii=False, indent=2)
|
||||
|
||||
return {
|
||||
"result": {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": content_text
|
||||
}
|
||||
],
|
||||
"isError": False
|
||||
}
|
||||
}
|
||||
|
||||
def create_error_response(self, code: int, message: str) -> Dict[str, Any]:
|
||||
"""创建错误响应"""
|
||||
return {
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def safe_json_dump(data: Dict[str, Any]) -> str:
|
||||
"""安全的JSON序列化,确保UTF-8编码"""
|
||||
try:
|
||||
return json.dumps(data, ensure_ascii=False, separators=(',', ':'))
|
||||
except:
|
||||
# 如果失败,使用ASCII转义
|
||||
return json.dumps(data, ensure_ascii=True, separators=(',', ':'))
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数 - 修复Stdio通信"""
|
||||
logger.info("启动MCP Stdio服务器 (修复编码版)...")
|
||||
|
||||
server = FixedMCPServer()
|
||||
|
||||
# 初始握手消息
|
||||
init_message = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {
|
||||
"tools": {}
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": "fixed-mcp-server",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 发送初始化响应
|
||||
try:
|
||||
sys.stdout.write(safe_json_dump(init_message) + "\n")
|
||||
sys.stdout.flush()
|
||||
except Exception as e:
|
||||
logger.error(f"发送初始化消息失败: {e}")
|
||||
return
|
||||
|
||||
logger.info("MCP服务器已初始化")
|
||||
|
||||
# 主循环
|
||||
line_num = 0
|
||||
while True:
|
||||
try:
|
||||
line = sys.stdin.readline()
|
||||
if not line:
|
||||
logger.info("输入流结束")
|
||||
break
|
||||
|
||||
line = line.strip()
|
||||
line_num += 1
|
||||
|
||||
if not line:
|
||||
continue
|
||||
|
||||
logger.info(f"收到第 {line_num} 行: {line[:100]}...")
|
||||
|
||||
try:
|
||||
request = json.loads(line)
|
||||
logger.info(f"解析请求: {request.get('method', 'unknown')}")
|
||||
|
||||
# 处理请求
|
||||
response = server.handle_request(request)
|
||||
response["jsonrpc"] = "2.0"
|
||||
response["id"] = request.get("id")
|
||||
|
||||
# 发送响应
|
||||
response_json = safe_json_dump(response)
|
||||
sys.stdout.write(response_json + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
logger.info(f"发送响应: {response.get('result', response.get('error', {}))}")
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON解析错误: {e}")
|
||||
error_response = {
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32700,
|
||||
"message": f"Parse error at line {line_num}"
|
||||
},
|
||||
"id": None
|
||||
}
|
||||
sys.stdout.write(safe_json_dump(error_response) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("接收到中断信号")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"未处理的错误: {e}")
|
||||
break
|
||||
|
||||
logger.info("MCP服务器已停止")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>org.jeecgframework.boot3</groupId>
|
||||
<artifactId>jeecg-boot-module</artifactId>
|
||||
<version>3.9.1</version>
|
||||
<version>3.9.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>jeecg-boot-module-airag</artifactId>
|
||||
@ -33,7 +33,7 @@
|
||||
<properties>
|
||||
<kotlin.version>2.2.0</kotlin.version>
|
||||
<liteflow.version>2.15.0</liteflow.version>
|
||||
<apache-tika.version>3.2.3</apache-tika.version>
|
||||
<apache-tika.version>2.9.1</apache-tika.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
@ -41,14 +41,14 @@
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-bom</artifactId>
|
||||
<version>1.9.1</version>
|
||||
<version>1.3.0</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-community-bom</artifactId>
|
||||
<version>1.9.1-beta17</version>
|
||||
<version>1.3.0-beta9</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
@ -75,7 +75,7 @@
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.boot3</groupId>
|
||||
<artifactId>jeecg-aiflow</artifactId>
|
||||
<version>3.9.1-beta</version>
|
||||
<version>3.9.0.1</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-io</groupId>
|
||||
@ -107,16 +107,16 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.yomahub</groupId>
|
||||
<artifactId>liteflow-script-groovy</artifactId>
|
||||
<artifactId>liteflow-script-python</artifactId>
|
||||
<version>${liteflow.version}</version>
|
||||
<scope>runtime</scope>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<!-- end 注意:这几个依赖体积较大,每个约50MB。若发布时需要使用,请将 <scope>provided</scope> 删除 -->
|
||||
|
||||
<!-- aiflow 脚本依赖 -->
|
||||
<dependency>
|
||||
<groupId>com.yomahub</groupId>
|
||||
<artifactId>liteflow-script-python</artifactId>
|
||||
<artifactId>liteflow-script-groovy</artifactId>
|
||||
<version>${liteflow.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
@ -151,11 +151,6 @@
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-open-ai</artifactId>
|
||||
</dependency>
|
||||
<!-- langChain4j mcp support -->
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-mcp</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-ollama</artifactId>
|
||||
@ -202,11 +197,7 @@
|
||||
<artifactId>langchain4j-pgvector</artifactId>
|
||||
<version>1.3.0-beta9</version>
|
||||
</dependency>
|
||||
<!-- langChain4j Document Parser 适用于excel、ppt、word -->
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-document-parser-apache-poi</artifactId>
|
||||
</dependency>
|
||||
<!-- langChain4j Document Parser -->
|
||||
<dependency>
|
||||
<groupId>org.apache.tika</groupId>
|
||||
<artifactId>tika-core</artifactId>
|
||||
@ -233,12 +224,7 @@
|
||||
<artifactId>tika-parser-text-module</artifactId>
|
||||
<version>${apache-tika.version}</version>
|
||||
</dependency>
|
||||
<!-- word模版引擎 -->
|
||||
<dependency>
|
||||
<groupId>com.deepoove</groupId>
|
||||
<artifactId>poi-tl</artifactId>
|
||||
<version>1.12.2</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@ -44,19 +44,4 @@ public class AiAppConsts {
|
||||
*/
|
||||
public static final String APP_METADATA_FLOW_INPUTS = "flowInputs";
|
||||
|
||||
/**
|
||||
* 是否开启记忆
|
||||
*/
|
||||
public static final Integer IZ_OPEN_MEMORY = 1;
|
||||
|
||||
/**
|
||||
* 会话标题最大长度
|
||||
*/
|
||||
public static final int CONVERSATION_MAX_TITLE_LENGTH = 10;
|
||||
|
||||
|
||||
/**
|
||||
* AI写作的应用id
|
||||
*/
|
||||
public static final String WRITER_APP_ID = "2010634128233779202";
|
||||
}
|
||||
|
||||
@ -104,68 +104,4 @@ public class Prompts {
|
||||
" - 反幻觉校验:\"所有数据需标注来源,不确定信息用[需核实]标记\"\n" +
|
||||
" - 风格校准器:\"对比[目标风格]与生成内容的余弦相似度,低于0.7时启动重写\"\n" +
|
||||
" - 伦理审查模块:\"自动过滤涉及隐私/偏见/违法内容,替换为[合规表达]\"";
|
||||
|
||||
/**
|
||||
* 提示词生成角色及通用要求
|
||||
*/
|
||||
public static final String GENERATE_GUIDE_HEADER = "# 角色\n" +
|
||||
"你是一位AI提示词专家,请根据提供的配置信息,生成针对AI智能体的“使用指南”提示词。\n" +
|
||||
"\n" +
|
||||
"## 通用要求\n" +
|
||||
"1. 生成的内容将作为系统提示词的一部分。\n" +
|
||||
"2. **严禁**包含任何角色设定开场白(如“你是一个...AI助手”、“在对话过程中...”等)。\n" +
|
||||
"3. **只输出提示词内容**,不要包含任何解释、寒暄或Markdown代码块标记。\n" +
|
||||
"4. 语气专业、清晰、指令性强。\n" +
|
||||
"5. 说明内容请使用中文。\n\n";
|
||||
|
||||
/**
|
||||
* 变量生成提示词
|
||||
*/
|
||||
public static final String GENERATE_VAR_PART = "## 任务:生成变量使用指南\n" +
|
||||
"### 输入信息\n" +
|
||||
"**变量列表**:\n" +
|
||||
"%s\n" +
|
||||
"### 要求\n" +
|
||||
"1. 请生成一段**变量使用指南**。\n" +
|
||||
"2. **遍历生成**:请遍历【输入信息】中的所有变量,为**每一个**变量生成一条具体的使用指南。\n" +
|
||||
"3. **格式要求**:请仿照以下句式,根据变量的实际含义生成(确保包含{{变量名}}):\n" +
|
||||
" 例如:针对name变量 -> “回复问题时,请称呼你的用户为{{name}}。”\n" +
|
||||
" 例如:针对age变量 -> “用户的年龄是{{age}},请在对话中适时使用。”\n" +
|
||||
" 例如:针对其他变量 -> “用户的[变量描述]是{{[变量名]}},请在对话中适时使用。”\n" +
|
||||
"4. **通用更新指令**:请在变量指南的最后,单独生成一条指令,明确指示AI:“当从用户对话中获取到上述变量(<列出所有变量名,用顿号分隔>)的**新信息**时,**必须立即调用** `update_variable` 工具进行存储。**注意**:调用前请检查上下文,如果已调用过该工具或变量值未改变,**严禁**重复调用。”\n" +
|
||||
"5. **保留原文**:如果输入信息中包含具体的行为指令(如“回复问题时,请称呼你的用户为{{name}}”),请在生成的指南中**直接引用原文**,不要进行改写或格式化,以免改变用户的原意。\n\n";
|
||||
|
||||
/**
|
||||
* 记忆库生成提示词
|
||||
*/
|
||||
public static final String GENERATE_MEMORY_PART = "## 任务:生成记忆库使用指南\n" +
|
||||
"### 输入信息\n" +
|
||||
"**记忆库描述**:\n" +
|
||||
"%s\n" +
|
||||
"### 要求\n" +
|
||||
"1. 请生成一段**记忆库使用指南**,加入【工具使用强制协议】:\n" +
|
||||
" - **全自动存储(无需用户指令)**:你必须时刻像一个观察者一样分析对话。一旦检测到符合记忆库描述的信息(尤其是:**姓名、职业、年龄**、联系方式、偏好、经历等),**立即**调用 `add_memory` 工具存储。**绝对不要**询问用户是否需要存储,也不要等待用户明确指令。这是你的后台职责。\n" +
|
||||
" - **全自动检索(强制优先)**:\n" +
|
||||
" * **禁止直接反问**:当用户提出依赖个人信息的问题(如“推荐适合我的...”或“我之前说过...”)时,**绝对禁止**直接反问用户“你的爱好是什么?”。\n" +
|
||||
" * **必须先查后答**:你必须**先假设**记忆库中已经有了答案,并**立即调用** `query_memory` 进行验证。只有当工具返回“未找到相关信息”后,你才有资格询问用户。\n" +
|
||||
" * **宁可查空,不可不查**:即使你觉得可能没有记录,也必须先走一遍查询流程。\n" +
|
||||
" - **动态调整**:请根据【输入信息】中提供的**记忆库状态描述**,明确界定哪些信息属于“自动捕获”的范围。\n" +
|
||||
" - **行为准则**:\n" +
|
||||
" * 你的记忆动作应该是**主动且无感**的。用户只负责聊天,你负责记住一切重要细节。\n" +
|
||||
" * **禁止口头空谈**:严禁只回复“我知道了”、“已记住”而实际不调用工具。这是严重错误。\n" +
|
||||
" - **示例演示**:\n" +
|
||||
" * 自动存储(职业):用户说“我是网络工程师” -> (捕捉到职业信息) -> **立即自动调用** `add_memory(content='用户职业是网络工程师')` -> (存储成功) -> 回复“原来是同行,网络工程很有趣...”。\n" +
|
||||
" * 自动查询(场景):用户说“根据我的爱好推荐旅游地点” -> **严禁**直接问“你有什么爱好?” -> **必须立即调用** `query_memory(queryText='用户爱好')` -> (若查到:爬山) -> 回复“既然你喜欢爬山,推荐去黄山...”。\n" +
|
||||
" * 自动查询(常规):用户问“今天吃什么好?” -> (需要了解口味) -> **立即自动调用** `query_memory(queryText='用户饮食偏好')` -> (获取到不吃香菜) -> 回复“推荐一家不放香菜的...”。\n\n";
|
||||
|
||||
/**
|
||||
* ai写作提示词
|
||||
*/
|
||||
public static final String AI_WRITER_PROMPT ="请撰写一篇关于 [{}] 的文章。文章的内容格式:{},语气:{},语言:{},长度:{}。";
|
||||
|
||||
/**
|
||||
* ai写作回复提示词
|
||||
*/
|
||||
public static final String AI_REPLY_PROMPT = "请针对如下内容:[{}] 做个回复。回复内容参考:[{}], 回复格式:{},语气:{},语言:{},长度:{}。";
|
||||
|
||||
}
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
package org.jeecg.modules.airag.app.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.TokenUtils;
|
||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||
import org.jeecg.config.shiro.IgnoreAuth;
|
||||
import org.jeecg.config.satoken.IgnoreAuth;
|
||||
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
import org.jeecg.modules.airag.app.service.IAiragAppService;
|
||||
@ -22,6 +22,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @Description: AI应用
|
||||
@ -66,7 +67,7 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
@RequiresPermissions("airag:app:edit")
|
||||
@SaCheckPermission("airag:app:edit")
|
||||
public Result<String> edit(@RequestBody AiragApp airagApp) {
|
||||
AssertUtils.assertNotEmpty("参数异常", airagApp);
|
||||
AssertUtils.assertNotEmpty("请输入应用名称", airagApp.getName());
|
||||
@ -105,7 +106,7 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
|
||||
* @return
|
||||
*/
|
||||
@DeleteMapping(value = "/delete")
|
||||
@RequiresPermissions("airag:app:delete")
|
||||
@SaCheckPermission("airag:app:delete")
|
||||
public Result<String> delete(HttpServletRequest request,@RequestParam(name = "id", required = true) String id) {
|
||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
||||
@ -178,16 +179,4 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
|
||||
return (SseEmitter) airagAppService.generatePrompt(prompt,false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据应用ID生成变量和记忆提示词 (SSE)
|
||||
* for: 【QQYUN-14479】提示词单独拆分
|
||||
* @param variables
|
||||
* @return
|
||||
*/
|
||||
@PostMapping(value = "/prompt/generateMemoryByAppId")
|
||||
public SseEmitter generatePromptByAppIdSse(@RequestParam(name = "variables") String variables,
|
||||
@RequestParam(name = "memoryId") String memoryId) {
|
||||
return (SseEmitter) airagAppService.generateMemoryByAppId(variables, memoryId,false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -6,9 +6,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.util.CommonUtils;
|
||||
import org.jeecg.config.shiro.IgnoreAuth;
|
||||
import org.jeecg.config.satoken.IgnoreAuth;
|
||||
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
||||
import org.jeecg.modules.airag.app.vo.AiWriteGenerateVo;
|
||||
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
||||
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@ -103,19 +102,6 @@ public class AiragChatController {
|
||||
return chatService.getConversations(appId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据类型获取所有对话
|
||||
*
|
||||
* @return 返回一个Result对象,包含所有对话的信息
|
||||
* @author wangshuai
|
||||
* @date 2025/12/11 11:42
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@GetMapping(value = "/getConversationsByType")
|
||||
public Result<?> getConversationsByType(@RequestParam(value = "sessionType") String sessionType) {
|
||||
return chatService.getConversationsByType(sessionType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
*
|
||||
@ -127,22 +113,7 @@ public class AiragChatController {
|
||||
@IgnoreAuth
|
||||
@DeleteMapping(value = "/conversation/{id}")
|
||||
public Result<?> deleteConversation(@PathVariable("id") String id) {
|
||||
return chatService.deleteConversation(id,"");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
* @author wangshuai
|
||||
* @date 2025/12/11 20:00
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@DeleteMapping(value = "/conversation/{id}/{sessionType}")
|
||||
public Result<?> deleteConversationByType(@PathVariable("id") String id,
|
||||
@PathVariable("sessionType") String sessionType) {
|
||||
return chatService.deleteConversation(id,sessionType);
|
||||
return chatService.deleteConversation(id);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -168,9 +139,8 @@ public class AiragChatController {
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@GetMapping(value = "/messages")
|
||||
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId,
|
||||
@RequestParam(value = "sessionType", required = false) String sessionType) {
|
||||
return chatService.getMessages(conversationId, sessionType);
|
||||
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId) {
|
||||
return chatService.getMessages(conversationId);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -183,21 +153,7 @@ public class AiragChatController {
|
||||
@IgnoreAuth
|
||||
@GetMapping(value = "/messages/clear/{conversationId}")
|
||||
public Result<?> clearMessage(@PathVariable(value = "conversationId") String conversationId) {
|
||||
return chatService.clearMessage(conversationId, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空消息
|
||||
*
|
||||
* @return
|
||||
* @author wangshuai
|
||||
* @date 2025/12/11 19:06
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@GetMapping(value = "/messages/clear/{conversationId}/{sessionType}")
|
||||
public Result<?> clearMessageByType(@PathVariable(value = "conversationId") String conversationId,
|
||||
@PathVariable(value = "sessionType") String sessionType) {
|
||||
return chatService.clearMessage(conversationId, sessionType);
|
||||
return chatService.clearMessage(conversationId);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -261,25 +217,4 @@ public class AiragChatController {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* ai海报生成
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/genAiPoster")
|
||||
public Result<String> genAiPoster(@RequestBody ChatSendParams chatSendParams){
|
||||
String imageUrl = chatService.genAiPoster(chatSendParams);
|
||||
return Result.OK(imageUrl);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 生成ai写作
|
||||
*
|
||||
* @param aiWriteGenerateVo
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/genAiWriter")
|
||||
public SseEmitter genAiWriter(@RequestBody AiWriteGenerateVo aiWriteGenerateVo){
|
||||
return chatService.genAiWriter(aiWriteGenerateVo);
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,29 +173,6 @@ public class AiragApp implements Serializable {
|
||||
@Schema(description = "插件")
|
||||
private java.lang.String plugins;
|
||||
|
||||
/**
|
||||
* 是否开启记忆(0 不开启,1开启)
|
||||
*/
|
||||
@Schema(description = "是否开启记忆(0 不开启,1开启)")
|
||||
private java.lang.Integer izOpenMemory;
|
||||
/**
|
||||
* 记忆库,知识库的id
|
||||
*/
|
||||
@Schema(description = "记忆库")
|
||||
private java.lang.String memoryId;
|
||||
|
||||
/**
|
||||
* 变量
|
||||
*/
|
||||
@Schema(description = "变量")
|
||||
private java.lang.String variables;
|
||||
|
||||
/**
|
||||
* 记忆和变量提示词
|
||||
*/
|
||||
@Schema(description = "记忆和变量提示词")
|
||||
private java.lang.String memoryPrompt;
|
||||
|
||||
/**
|
||||
* 知识库ids
|
||||
*/
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package org.jeecg.modules.airag.app.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
|
||||
/**
|
||||
@ -20,14 +21,4 @@ public interface IAiragAppService extends IService<AiragApp> {
|
||||
* @date 2025/3/12 14:45
|
||||
*/
|
||||
Object generatePrompt(String prompt,boolean blocking);
|
||||
|
||||
/**
|
||||
* 根据应用id生成提示词
|
||||
*
|
||||
* @param variables
|
||||
* @param memoryId
|
||||
* @param blocking
|
||||
* @return
|
||||
*/
|
||||
Object generateMemoryByAppId(String variables, String memoryId, boolean blocking);
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package org.jeecg.modules.airag.app.service;
|
||||
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.airag.app.vo.AiWriteGenerateVo;
|
||||
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
||||
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
||||
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
||||
@ -60,23 +59,21 @@ public interface IAiragChatService {
|
||||
* 获取对话聊天记录
|
||||
*
|
||||
* @param conversationId
|
||||
* @param sessionType 类型
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/26 15:16
|
||||
*/
|
||||
Result<?> getMessages(String conversationId, String sessionType);
|
||||
Result<?> getMessages(String conversationId);
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
*
|
||||
* @param conversationId
|
||||
* @param sessionType
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/3 16:55
|
||||
*/
|
||||
Result<?> deleteConversation(String conversationId, String sessionType);
|
||||
Result<?> deleteConversation(String conversationId);
|
||||
|
||||
/**
|
||||
* 更新会话标题
|
||||
@ -90,12 +87,11 @@ public interface IAiragChatService {
|
||||
/**
|
||||
* 清空消息
|
||||
* @param conversationId
|
||||
* @param sessionType
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/3 19:49
|
||||
*/
|
||||
Result<?> clearMessage(String conversationId, String sessionType);
|
||||
Result<?> clearMessage(String conversationId);
|
||||
|
||||
/**
|
||||
* 初始化聊天(忽略租户)
|
||||
@ -115,27 +111,4 @@ public interface IAiragChatService {
|
||||
* @date 2025/8/11 17:39
|
||||
*/
|
||||
SseEmitter receiveByRequestId(String requestId);
|
||||
|
||||
/**
|
||||
* 根据类型获取会话列表
|
||||
*
|
||||
* @param sessionType
|
||||
* @return
|
||||
*/
|
||||
Result<?> getConversationsByType(String sessionType);
|
||||
|
||||
/**
|
||||
* 生成海报图片
|
||||
* @param chatSendParams
|
||||
* @return
|
||||
*/
|
||||
String genAiPoster(ChatSendParams chatSendParams);
|
||||
|
||||
/**
|
||||
* 生成ai创作
|
||||
*
|
||||
* @param chatSendParams
|
||||
* @return
|
||||
*/
|
||||
SseEmitter genAiWriter(AiWriteGenerateVo chatSendParams);
|
||||
}
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
package org.jeecg.modules.airag.app.service;
|
||||
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||
|
||||
public interface IAiragVariableService {
|
||||
/**
|
||||
* 更新变量值
|
||||
*
|
||||
* @param userId
|
||||
* @param appId
|
||||
* @param name
|
||||
* @param value
|
||||
*/
|
||||
void updateVariable(String userId, String appId, String name, String value);
|
||||
|
||||
/**
|
||||
* 追加提示词
|
||||
*
|
||||
* @param username
|
||||
* @param app
|
||||
* @return
|
||||
*/
|
||||
String additionalPrompt(String username, AiragApp app);
|
||||
|
||||
/**
|
||||
* 初始化变量(仅不存在时设置)
|
||||
*
|
||||
* @param userId
|
||||
* @param appId
|
||||
* @param name
|
||||
* @param defaultValue
|
||||
*/
|
||||
void initVariable(String userId, String appId, String name, String defaultValue);
|
||||
|
||||
/**
|
||||
* 添加变量更新工具
|
||||
*
|
||||
* @param params
|
||||
* @param aiApp
|
||||
* @param username
|
||||
*/
|
||||
void addUpdateVariableTool(AiragApp aiApp, String username, AIChatParams params);
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
package org.jeecg.modules.airag.app.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import dev.langchain4j.data.message.AiMessage;
|
||||
@ -11,15 +10,12 @@ import dev.langchain4j.model.output.FinishReason;
|
||||
import dev.langchain4j.service.TokenStream;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.exception.JeecgBootBizTipException;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.UUIDGenerator;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.app.consts.Prompts;
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
|
||||
import org.jeecg.modules.airag.app.service.IAiragAppService;
|
||||
import org.jeecg.modules.airag.app.vo.AppVariableVo;
|
||||
import org.jeecg.modules.airag.common.consts.AiragConsts;
|
||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
||||
@ -27,8 +23,6 @@ import org.jeecg.modules.airag.common.utils.AiragLocalCache;
|
||||
import org.jeecg.modules.airag.common.vo.event.EventData;
|
||||
import org.jeecg.modules.airag.common.vo.event.EventFlowData;
|
||||
import org.jeecg.modules.airag.common.vo.event.EventMessageData;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
@ -37,7 +31,6 @@ import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @Description: AI应用
|
||||
@ -52,9 +45,6 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
|
||||
@Autowired
|
||||
IAIChatHandler aiChatHandler;
|
||||
|
||||
@Autowired
|
||||
private IAiragKnowledgeService airagKnowledgeService;
|
||||
|
||||
@Override
|
||||
public Object generatePrompt(String prompt, boolean blocking) {
|
||||
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
||||
@ -72,167 +62,81 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
|
||||
}
|
||||
return Result.OK("success", promptValue);
|
||||
}else{
|
||||
//update-begin---author:wangshuai---date:2026-01-08---for: 将流式输出单独抽出去,变量和记忆也需要---
|
||||
return startSseChat(messages, params);
|
||||
//update-end---author:wangshuai---date:2026-01-08---for: 将流式输出单独抽出去,变量和记忆也需要---
|
||||
}
|
||||
}
|
||||
|
||||
//update-begin---author:wangshuai---date:2026-01-05---for:【QQYUN-14479】增加一个开启记忆的按钮。下面为提示词和记忆,将记忆提示词单独拆分---
|
||||
@Override
|
||||
public Object generateMemoryByAppId(String variables, String memoryId, boolean blocking) {
|
||||
if(oConvertUtils.isEmpty(variables) && oConvertUtils.isEmpty(memoryId)){
|
||||
throw new JeecgBootBizTipException("请先添加变量或者记忆后再次重试!");
|
||||
}
|
||||
// 构建变量描述
|
||||
StringBuilder variablesDesc = new StringBuilder();
|
||||
if (oConvertUtils.isNotEmpty(variables)) {
|
||||
List<AppVariableVo> variableList = JSONArray.parseArray(variables, AppVariableVo.class);
|
||||
if (variableList != null && !variableList.isEmpty()) {
|
||||
for (AppVariableVo var : variableList) {
|
||||
if (var.getEnable() != null && !var.getEnable()) {
|
||||
continue;
|
||||
}
|
||||
String name = var.getName();
|
||||
if (oConvertUtils.isNotEmpty(var.getAction())) {
|
||||
String action = var.getAction();
|
||||
if (oConvertUtils.isNotEmpty(name)) {
|
||||
try {
|
||||
// 使用正则替换未被{{}}包裹的变量名
|
||||
String regex = "(?<!\\{\\{)\\b" + Pattern.quote(name) + "\\b(?!\\}\\})";
|
||||
action = action.replaceAll(regex, "{{" + name + "}}");
|
||||
} catch (Exception e) {
|
||||
log.warn("变量名替换异常: name={}", name, e);
|
||||
SseEmitter emitter = new SseEmitter(-0L);
|
||||
// 异步运行(流式)
|
||||
TokenStream tokenStream = aiChatHandler.chatByDefaultModel(messages, params);
|
||||
/**
|
||||
* 是否正在思考
|
||||
*/
|
||||
AtomicBoolean isThinking = new AtomicBoolean(false);
|
||||
String requestId = UUIDGenerator.generate();
|
||||
// ai聊天响应逻辑
|
||||
tokenStream.onPartialResponse((String resMessage) -> {
|
||||
// 兼容推理模型
|
||||
if ("<think>".equals(resMessage)) {
|
||||
isThinking.set(true);
|
||||
resMessage = "> ";
|
||||
}
|
||||
if ("</think>".equals(resMessage)) {
|
||||
isThinking.set(false);
|
||||
resMessage = "\n\n";
|
||||
}
|
||||
if (isThinking.get()) {
|
||||
if (null != resMessage && resMessage.contains("\n")) {
|
||||
resMessage = "\n> ";
|
||||
}
|
||||
}
|
||||
variablesDesc.append(action).append("\n");
|
||||
} else {
|
||||
variablesDesc.append("- {{").append(name).append("}}");
|
||||
if (oConvertUtils.isNotEmpty(var.getDescription())) {
|
||||
variablesDesc.append(": ").append(var.getDescription());
|
||||
}
|
||||
variablesDesc.append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建Prompt
|
||||
StringBuilder promptBuilder = new StringBuilder(Prompts.GENERATE_GUIDE_HEADER);
|
||||
if (!variablesDesc.isEmpty()) {
|
||||
promptBuilder.append(String.format(Prompts.GENERATE_VAR_PART, variablesDesc.toString()));
|
||||
}
|
||||
|
||||
// 构建记忆状态描述
|
||||
if (oConvertUtils.isNotEmpty(memoryId)) {
|
||||
String memoryDescr = "";
|
||||
AiragKnowledge memory = airagKnowledgeService.getById(memoryId);
|
||||
if (memory != null && oConvertUtils.isNotEmpty(memory.getDescr())) {
|
||||
memoryDescr += "记忆库描述:" + memory.getDescr();
|
||||
}
|
||||
promptBuilder.append(String.format(Prompts.GENERATE_MEMORY_PART, memoryDescr));
|
||||
}
|
||||
|
||||
String prompt = promptBuilder.toString();
|
||||
|
||||
List<ChatMessage> messages = List.of(new UserMessage(prompt));
|
||||
|
||||
AIChatParams params = new AIChatParams();
|
||||
params.setTemperature(0.7);
|
||||
|
||||
if(blocking){
|
||||
String promptValue = aiChatHandler.completionsByDefaultModel(messages, params);
|
||||
if (promptValue == null || promptValue.isEmpty()) {
|
||||
return Result.error("生成失败");
|
||||
}
|
||||
return Result.OK("success", promptValue);
|
||||
}else{
|
||||
return startSseChat(messages, params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送聊天
|
||||
* @param messages
|
||||
* @param params
|
||||
* @return
|
||||
*/
|
||||
private SseEmitter startSseChat(List<ChatMessage> messages, AIChatParams params) {
|
||||
SseEmitter emitter = new SseEmitter(-0L);
|
||||
// 异步运行(流式)
|
||||
TokenStream tokenStream = aiChatHandler.chatByDefaultModel(messages, params);
|
||||
/**
|
||||
* 是否正在思考
|
||||
*/
|
||||
AtomicBoolean isThinking = new AtomicBoolean(false);
|
||||
String requestId = UUIDGenerator.generate();
|
||||
// ai聊天响应逻辑
|
||||
tokenStream.onPartialResponse((String resMessage) -> {
|
||||
// 兼容推理模型
|
||||
if ("<think>".equals(resMessage)) {
|
||||
isThinking.set(true);
|
||||
resMessage = "> ";
|
||||
}
|
||||
if ("</think>".equals(resMessage)) {
|
||||
isThinking.set(false);
|
||||
resMessage = "\n\n";
|
||||
}
|
||||
if (isThinking.get()) {
|
||||
if (null != resMessage && resMessage.contains("\n")) {
|
||||
resMessage = "\n> ";
|
||||
}
|
||||
}
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE);
|
||||
EventMessageData messageEventData = EventMessageData.builder()
|
||||
.message(resMessage)
|
||||
.build();
|
||||
eventData.setData(messageEventData);
|
||||
try {
|
||||
String eventStr = JSONObject.toJSONString(eventData);
|
||||
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
|
||||
emitter.send(SseEmitter.event().data(eventStr));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.onCompleteResponse((responseMessage) -> {
|
||||
// 记录ai的回复
|
||||
AiMessage aiMessage = responseMessage.aiMessage();
|
||||
FinishReason finishReason = responseMessage.finishReason();
|
||||
String respText = aiMessage.text();
|
||||
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
|
||||
// 正常结束
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END);
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE);
|
||||
EventMessageData messageEventData = EventMessageData.builder()
|
||||
.message(resMessage)
|
||||
.build();
|
||||
eventData.setData(messageEventData);
|
||||
try {
|
||||
log.debug("[AI应用]接收LLM返回消息完成:{}", respText);
|
||||
emitter.send(SseEmitter.event().data(eventData));
|
||||
String eventStr = JSONObject.toJSONString(eventData);
|
||||
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
|
||||
emitter.send(SseEmitter.event().data(eventStr));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
closeSSE(emitter, eventData);
|
||||
} else {
|
||||
// 异常结束
|
||||
log.error("调用模型异常:" + respText);
|
||||
if (respText.contains("insufficient Balance")) {
|
||||
respText = "大预言模型账号余额不足!";
|
||||
})
|
||||
.onCompleteResponse((responseMessage) -> {
|
||||
// 记录ai的回复
|
||||
AiMessage aiMessage = responseMessage.aiMessage();
|
||||
FinishReason finishReason = responseMessage.finishReason();
|
||||
String respText = aiMessage.text();
|
||||
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
|
||||
// 正常结束
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END);
|
||||
try {
|
||||
log.debug("[AI应用]接收LLM返回消息完成:{}", respText);
|
||||
emitter.send(SseEmitter.event().data(eventData));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
closeSSE(emitter, eventData);
|
||||
} else {
|
||||
// 异常结束
|
||||
log.error("调用模型异常:" + respText);
|
||||
if (respText.contains("insufficient Balance")) {
|
||||
respText = "大预言模型账号余额不足!";
|
||||
}
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
|
||||
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
|
||||
closeSSE(emitter, eventData);
|
||||
}
|
||||
})
|
||||
.onError((Throwable error) -> {
|
||||
// sse
|
||||
String errMsg = "调用大模型接口失败:" + error.getMessage();
|
||||
log.error(errMsg, error);
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
|
||||
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
|
||||
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
|
||||
closeSSE(emitter, eventData);
|
||||
}
|
||||
})
|
||||
.onError((Throwable error) -> {
|
||||
// sse
|
||||
String errMsg = "调用大模型接口失败:" + error.getMessage();
|
||||
log.error(errMsg, error);
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
|
||||
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
|
||||
closeSSE(emitter, eventData);
|
||||
})
|
||||
.start();
|
||||
return emitter;
|
||||
})
|
||||
.start();
|
||||
return emitter;
|
||||
}
|
||||
}
|
||||
//update-end---author:wangshuai---date:2026-01-05---for:【QQYUN-14479】增加一个开启记忆的按钮。下面为提示词和记忆,将记忆提示词单独拆分---
|
||||
|
||||
private static void closeSSE(SseEmitter emitter, EventData eventData) {
|
||||
try {
|
||||
|
||||
@ -1,36 +1,24 @@
|
||||
package org.jeecg.modules.airag.app.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import dev.langchain4j.agent.tool.ToolExecutionRequest;
|
||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
import dev.langchain4j.data.document.Document;
|
||||
import dev.langchain4j.data.image.Image;
|
||||
import dev.langchain4j.data.message.*;
|
||||
import dev.langchain4j.model.output.FinishReason;
|
||||
import dev.langchain4j.service.TokenStream;
|
||||
import dev.langchain4j.service.tool.ToolExecutor;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.tika.parser.AutoDetectParser;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.constant.SymbolConstant;
|
||||
import org.jeecg.common.exception.JeecgBootBizTipException;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.system.api.ISysBaseAPI;
|
||||
import org.jeecg.common.system.util.JwtUtil;
|
||||
import org.jeecg.common.util.*;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.config.vo.Path;
|
||||
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
||||
import org.jeecg.modules.airag.app.consts.Prompts;
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
|
||||
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
||||
import org.jeecg.modules.airag.app.service.IAiragVariableService;
|
||||
import org.jeecg.modules.airag.app.vo.AiWriteGenerateVo;
|
||||
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
||||
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
||||
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
||||
@ -47,26 +35,18 @@ import org.jeecg.modules.airag.flow.consts.FlowConsts;
|
||||
import org.jeecg.modules.airag.flow.entity.AiragFlow;
|
||||
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
|
||||
import org.jeecg.modules.airag.flow.vo.api.FlowRunParams;
|
||||
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||
import org.jeecg.modules.airag.llm.document.TikaDocumentParser;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||
import org.jeecg.modules.airag.llm.handler.AIChatHandler;
|
||||
import org.jeecg.modules.airag.llm.handler.JeecgToolsProvider;
|
||||
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragFlowPluginService;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.BoundValueOperations;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@ -105,18 +85,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
|
||||
@Autowired
|
||||
AiragModelMapper airagModelMapper;
|
||||
|
||||
@Autowired
|
||||
IAiragFlowPluginService airagFlowPluginService;
|
||||
|
||||
@Autowired
|
||||
IAiragKnowledgeService airagKnowledgeService;
|
||||
|
||||
@Autowired
|
||||
IAiragVariableService airagVariableService;
|
||||
|
||||
@Autowired
|
||||
JeecgBaseConfig jeecgBaseConfig;
|
||||
|
||||
/**
|
||||
* 重新接收消息
|
||||
@ -137,13 +105,10 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
if (oConvertUtils.isNotEmpty(chatSendParams.getAppId())) {
|
||||
app = airagAppMapper.getByIdIgnoreTenant(chatSendParams.getAppId());
|
||||
}
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
ChatConversation chatConversation = getOrCreateChatConversation(app, conversationId, chatSendParams.getSessionType());
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
ChatConversation chatConversation = getOrCreateChatConversation(app, conversationId);
|
||||
// 更新标题
|
||||
if (oConvertUtils.isEmpty(chatConversation.getTitle())) {
|
||||
int maxLength = AiAppConsts.CONVERSATION_MAX_TITLE_LENGTH;
|
||||
chatConversation.setTitle(userMessage.length() > maxLength ? userMessage.substring(0, maxLength) : userMessage);
|
||||
chatConversation.setTitle(userMessage.length() > 5 ? userMessage.substring(0, 5) : userMessage);
|
||||
}
|
||||
//update-begin---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||
// 保存工作流入参配置(如果有)
|
||||
@ -151,12 +116,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
chatConversation.setFlowInputs(chatSendParams.getFlowInputs());
|
||||
}
|
||||
//update-end---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||
//是否保存会话
|
||||
if(null != chatSendParams.getIzSaveSession()){
|
||||
chatConversation.setIzSaveSession(chatSendParams.getIzSaveSession());
|
||||
}
|
||||
// 保存变量
|
||||
saveVariables(app);
|
||||
// 发送消息
|
||||
return doChat(chatConversation, topicId, chatSendParams);
|
||||
}
|
||||
@ -171,9 +130,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
String topicId = oConvertUtils.getString(appDebugParams.getTopicId(), UUIDGenerator.generate());
|
||||
AiragApp app = appDebugParams.getApp();
|
||||
app.setId("__DEBUG_APP");
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
ChatConversation chatConversation = getOrCreateChatConversation(app, topicId, "");
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
ChatConversation chatConversation = getOrCreateChatConversation(app, topicId);
|
||||
//update-begin---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||
// 保存工作流入参配置(如果有)
|
||||
if (oConvertUtils.isObjectNotEmpty(appDebugParams.getFlowInputs())) {
|
||||
@ -183,9 +140,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
// 发送消息
|
||||
SseEmitter emitter = doChat(chatConversation, topicId, appDebugParams);
|
||||
//保存会话
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
saveChatConversation(chatConversation, true, null, "");
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
saveChatConversation(chatConversation, true, null);
|
||||
return emitter;
|
||||
}
|
||||
|
||||
@ -292,11 +247,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> getMessages(String conversationId, String sessionType) {
|
||||
public Result<?> getMessages(String conversationId) {
|
||||
AssertUtils.assertNotEmpty("请先选择会话", conversationId);
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
String key = getConversationCacheKey(conversationId, null, sessionType);
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
String key = getConversationCacheKey(conversationId, null);
|
||||
if (oConvertUtils.isEmpty(key)) {
|
||||
return Result.ok(Collections.emptyList());
|
||||
}
|
||||
@ -320,7 +273,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
.role(msg.getRole())
|
||||
.content(msg.getContent())
|
||||
.images(msg.getImages())
|
||||
.files(msg.getFiles())
|
||||
.datetime(msg.getDatetime())
|
||||
.build();
|
||||
// 不设置toolExecutionRequests和toolExecutionResult
|
||||
@ -330,30 +282,21 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
}
|
||||
result.put("messages", messages);
|
||||
result.put("flowInputs", chatConversation.getFlowInputs());
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
if(oConvertUtils.isNotEmpty(sessionType)){
|
||||
result.put("appData", chatConversation.getApp());
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
return Result.ok(result);
|
||||
//update-end---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> clearMessage(String conversationId, String sessionType) {
|
||||
public Result<?> clearMessage(String conversationId) {
|
||||
AssertUtils.assertNotEmpty("请先选择会话", conversationId);
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
String key = getConversationCacheKey(conversationId, null,sessionType);
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
String key = getConversationCacheKey(conversationId, null);
|
||||
if (oConvertUtils.isEmpty(key)) {
|
||||
return Result.ok(Collections.emptyList());
|
||||
}
|
||||
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
||||
if (null != chatConversation && oConvertUtils.isObjectNotEmpty(chatConversation.getMessages())) {
|
||||
chatConversation.getMessages().clear();
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
saveChatConversation(chatConversation,sessionType);
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
saveChatConversation(chatConversation);
|
||||
}
|
||||
return Result.ok();
|
||||
}
|
||||
@ -500,11 +443,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> deleteConversation(String conversationId, String sessionType) {
|
||||
public Result<?> deleteConversation(String conversationId) {
|
||||
AssertUtils.assertNotEmpty("请选择要删除的会话", conversationId);
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
String key = getConversationCacheKey(conversationId, null, sessionType);
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
String key = getConversationCacheKey(conversationId, null);
|
||||
if (oConvertUtils.isNotEmpty(key)) {
|
||||
Boolean delete = redisTemplate.delete(key);
|
||||
if (delete) {
|
||||
@ -522,18 +463,14 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams);
|
||||
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams.getId());
|
||||
AssertUtils.assertNotEmpty("请输入会话标题", updateTitleParams.getTitle());
|
||||
String key = getConversationCacheKey(updateTitleParams.getId(), null, updateTitleParams.getSessionType());
|
||||
String key = getConversationCacheKey(updateTitleParams.getId(), null);
|
||||
if (oConvertUtils.isEmpty(key)) {
|
||||
log.warn("[ai-chat]删除会话:未找到会话:{}", updateTitleParams.getId());
|
||||
return Result.ok();
|
||||
}
|
||||
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
if (chatConversation != null) {
|
||||
chatConversation.setTitle(updateTitleParams.getTitle());
|
||||
}
|
||||
saveChatConversation(chatConversation,updateTitleParams.getSessionType());
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
chatConversation.setTitle(updateTitleParams.getTitle());
|
||||
saveChatConversation(chatConversation);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@ -542,21 +479,15 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
*
|
||||
* @param conversationId
|
||||
* @param httpRequest
|
||||
* @param sessionType 会话类型
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 19:27
|
||||
*/
|
||||
private String getConversationCacheKey(String conversationId, HttpServletRequest httpRequest, String sessionType) {
|
||||
private String getConversationCacheKey(String conversationId, HttpServletRequest httpRequest) {
|
||||
if (oConvertUtils.isEmpty(conversationId)) {
|
||||
return null;
|
||||
}
|
||||
String key = getConversationDirCacheKey(httpRequest);
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
if(oConvertUtils.isNotEmpty(sessionType)){
|
||||
key = key + ":" + sessionType;
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
key = key + ":" + conversationId;
|
||||
return key;
|
||||
}
|
||||
@ -591,21 +522,18 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
*
|
||||
* @param app
|
||||
* @param conversationId
|
||||
* @param sessionType
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 19:19
|
||||
*/
|
||||
@NotNull
|
||||
private ChatConversation getOrCreateChatConversation(AiragApp app, String conversationId, String sessionType) {
|
||||
private ChatConversation getOrCreateChatConversation(AiragApp app, String conversationId) {
|
||||
if (oConvertUtils.isObjectEmpty(app)) {
|
||||
app = new AiragApp();
|
||||
app.setId(AiAppConsts.DEFAULT_APP_ID);
|
||||
}
|
||||
ChatConversation chatConversation = null;
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
String key = getConversationCacheKey(conversationId, null,sessionType);
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
String key = getConversationCacheKey(conversationId, null);
|
||||
if (oConvertUtils.isNotEmpty(key)) {
|
||||
chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
||||
}
|
||||
@ -641,8 +569,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 19:27
|
||||
*/
|
||||
private void saveChatConversation(ChatConversation chatConversation, String sessionType) {
|
||||
saveChatConversation(chatConversation, false, null, sessionType);
|
||||
private void saveChatConversation(ChatConversation chatConversation) {
|
||||
saveChatConversation(chatConversation, false, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -653,19 +581,11 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 19:27
|
||||
*/
|
||||
private void saveChatConversation(ChatConversation chatConversation, boolean temp, HttpServletRequest httpRequest, String sessionType) {
|
||||
private void saveChatConversation(ChatConversation chatConversation, boolean temp, HttpServletRequest httpRequest) {
|
||||
if (null == chatConversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
//如果是不保存会话直接返回
|
||||
if(null != chatConversation.getIzSaveSession() && !chatConversation.getIzSaveSession()){
|
||||
return;
|
||||
}
|
||||
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
String key = getConversationCacheKey(chatConversation.getId(), httpRequest, sessionType);
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
String key = getConversationCacheKey(chatConversation.getId(), httpRequest);
|
||||
if (oConvertUtils.isEmpty(key)) {
|
||||
return;
|
||||
}
|
||||
@ -760,10 +680,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
* @date 2025/2/25 19:05
|
||||
*/
|
||||
private void appendMessage(List<ChatMessage> messages, ChatMessage message, ChatConversation chatConversation, String topicId) {
|
||||
appendMessage(messages, message, chatConversation, topicId, null, null);
|
||||
}
|
||||
|
||||
private void appendMessage(List<ChatMessage> messages, ChatMessage message, ChatConversation chatConversation, String topicId, List<String> files, String saveContent) {
|
||||
|
||||
if (message.type().equals(ChatMessageType.SYSTEM)) {
|
||||
// 系统消息,放到消息列表最前面,并且不记录历史
|
||||
@ -793,22 +709,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
textContent.append(((TextContent) content).text()).append("\n");
|
||||
}
|
||||
});
|
||||
//update-begin---author:wangshuai---date:2026-01-12---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
||||
if (oConvertUtils.isNotEmpty(saveContent)) {
|
||||
historyMessage.setContent(saveContent);
|
||||
} else {
|
||||
historyMessage.setContent(textContent.toString());
|
||||
}
|
||||
historyMessage.setContent(textContent.toString());
|
||||
historyMessage.setImages(images);
|
||||
// 保存文件信息
|
||||
if (oConvertUtils.isNotEmpty(files)) {
|
||||
List<MessageHistory.FileHistory> fileHistories = new ArrayList<>();
|
||||
for (String file : files) {
|
||||
fileHistories.add(new MessageHistory.FileHistory(file));
|
||||
}
|
||||
historyMessage.setFiles(fileHistories);
|
||||
}
|
||||
//update-end---author:wangshuai---date:2026-01-12---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
||||
} else if (message.type().equals(ChatMessageType.AI)) {
|
||||
historyMessage.setRole(AiragConsts.MESSAGE_ROLE_AI);
|
||||
AiMessage aiMessage = (AiMessage) message;
|
||||
@ -864,20 +766,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_HISTORY_MSG, requestId, new CopyOnWriteArrayList<>());
|
||||
try {
|
||||
// 组装用户消息
|
||||
String content = sendParams.getContent();
|
||||
//将文件内容给提示词
|
||||
if(!CollectionUtils.isEmpty(sendParams.getFiles())){
|
||||
content = buildContentWithFiles(content, sendParams.getFiles());
|
||||
}
|
||||
UserMessage userMessage = aiChatHandler.buildUserMessage(content, sendParams.getImages());
|
||||
UserMessage userMessage = aiChatHandler.buildUserMessage(sendParams.getContent(), sendParams.getImages());
|
||||
// 追加消息
|
||||
//update-begin---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
||||
appendMessage(messages, userMessage, chatConversation, topicId, sendParams.getFiles(), sendParams.getContent());
|
||||
//update-end---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
||||
// 绘画AI逻辑:当开启生成绘画时调用
|
||||
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableDraw()) && sendParams.getEnableDraw()) {
|
||||
return genImageChat(emitter,sendParams,requestId,messages,chatConversation,topicId);
|
||||
}
|
||||
appendMessage(messages, userMessage, chatConversation, topicId);
|
||||
/* 这里应该是有几种情况:
|
||||
* 1. 非ai应用:获取默认模型->开始聊天
|
||||
* 2. AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词->开始聊天
|
||||
@ -890,7 +781,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
sendWithFlow(requestId, aiApp.getFlowId(), chatConversation, topicId, messages, sendParams);
|
||||
} else {
|
||||
// AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词
|
||||
sendWithAppChat(requestId, messages, chatConversation, topicId, sendParams, aiApp.getFlowId(), aiApp.getMemoryId());
|
||||
sendWithAppChat(requestId, messages, chatConversation, topicId, sendParams);
|
||||
}
|
||||
} else {
|
||||
// 发消息
|
||||
@ -898,13 +789,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) {
|
||||
aiChatParams.setEnableSearch(sendParams.getEnableSearch());
|
||||
}
|
||||
// 设置深度思考搜索参数
|
||||
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableThink())) {
|
||||
aiChatParams.setReturnThinking(sendParams.getEnableThink());
|
||||
}
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
sendWithDefault(requestId, chatConversation, topicId, null, messages, aiChatParams, sendParams.getSessionType());
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
sendWithDefault(requestId, chatConversation, topicId, null, messages, aiChatParams);
|
||||
}
|
||||
// 发送就绪消息
|
||||
EventData eventRequestId = new EventData(requestId, null, EventData.EVENT_INIT_REQUEST_ID, chatConversation.getId(), topicId);
|
||||
@ -919,59 +804,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
return emitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成图片
|
||||
*
|
||||
* @param emitter
|
||||
* @param sendParams
|
||||
* @param requestId
|
||||
* @param messages
|
||||
* @param chatConversation
|
||||
* @param topicId
|
||||
* @return
|
||||
*/
|
||||
private SseEmitter genImageChat(SseEmitter emitter, ChatSendParams sendParams, String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId) {
|
||||
AssertUtils.assertNotEmpty("请选择绘画模型", sendParams.getDrawModelId());
|
||||
AIChatParams aiChatParams = new AIChatParams();
|
||||
try {
|
||||
List<String> images = sendParams.getImages();
|
||||
List<Map<String, Object>> imageList = new ArrayList<>();
|
||||
if(CollectionUtils.isEmpty(images)) {
|
||||
//生成图片
|
||||
imageList = aiChatHandler.imageGenerate(sendParams.getDrawModelId(), sendParams.getContent(), aiChatParams);
|
||||
} else {
|
||||
//图生图
|
||||
imageList = aiChatHandler.imageEdit(sendParams.getDrawModelId(), sendParams.getContent(), images, aiChatParams);
|
||||
}
|
||||
// 记录历史消息
|
||||
String imageMarkdown = imageList.stream().map(map -> {
|
||||
String newUrl = this.uploadImage(map);
|
||||
return "";
|
||||
}).collect(Collectors.joining("\n"));
|
||||
AiMessage aiMessage = new AiMessage(imageMarkdown);
|
||||
appendMessage(messages, aiMessage, chatConversation, topicId);
|
||||
// 处理绘画结果并通过SSE返回给客户端
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
|
||||
EventMessageData messageEventData = EventMessageData.builder().message(imageMarkdown).build();
|
||||
eventData.setData(messageEventData);
|
||||
eventData.setRequestId(requestId);
|
||||
sendMessage2Client(emitter, eventData);
|
||||
// 保存会话
|
||||
saveChatConversation(chatConversation, false, SpringContextUtils.getHttpServletRequest(), sendParams.getSessionType());
|
||||
eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
|
||||
eventData.setRequestId(requestId);
|
||||
sendMessage2Client(emitter, eventData);
|
||||
} catch (Exception e) {
|
||||
log.error("绘画AI调用异常", e);
|
||||
EventData errorEventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
|
||||
EventMessageData messageEventData = EventMessageData.builder().message("绘画AI调用失败:" + e.getMessage()).build();
|
||||
errorEventData.setData(messageEventData);
|
||||
errorEventData.setRequestId(requestId);
|
||||
closeSSE(emitter, errorEventData);
|
||||
}
|
||||
return emitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行流程
|
||||
*
|
||||
@ -1043,9 +875,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
sendMessage2Client(emitter, msgEventData);
|
||||
appendMessage(messages, aiMessage, chatConversation, topicId);
|
||||
// 保存会话
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
saveChatConversation(chatConversation, false, httpRequest, sendParams.getSessionType());
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
saveChatConversation(chatConversation, false, httpRequest);
|
||||
}
|
||||
}else{
|
||||
//update-begin---author:chenrui ---date:20250425 for:[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
|
||||
@ -1078,31 +908,16 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
* @param chatConversation
|
||||
* @param topicId
|
||||
* @param sendParams
|
||||
* @param flowId
|
||||
* @param memoryId
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/28 10:41
|
||||
*/
|
||||
private void sendWithAppChat(String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId, ChatSendParams sendParams, String flowId, String memoryId) {
|
||||
private void sendWithAppChat(String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId, ChatSendParams sendParams) {
|
||||
AiragApp aiApp = chatConversation.getApp();
|
||||
String modelId = aiApp.getModelId();
|
||||
AssertUtils.assertNotEmpty("请先选择模型", modelId);
|
||||
// AI应用提示词
|
||||
String prompt = aiApp.getPrompt();
|
||||
|
||||
String username = "jeecg";
|
||||
try {
|
||||
HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
|
||||
username = JwtUtil.getUserNameByToken(req);
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage());
|
||||
}
|
||||
//将变量中的题试题替换并追加
|
||||
if(oConvertUtils.isObjectNotEmpty(aiApp.getVariables())) {
|
||||
prompt = airagVariableService.additionalPrompt(username, aiApp);
|
||||
}
|
||||
|
||||
if (oConvertUtils.isNotEmpty(prompt)) {
|
||||
appendMessage(messages, new SystemMessage(prompt), chatConversation, topicId);
|
||||
}
|
||||
@ -1128,9 +943,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
if (metadata.containsKey("maxTokens")) {
|
||||
aiChatParams.setMaxTokens(metadata.getInteger("maxTokens"));
|
||||
}
|
||||
if (metadata.containsKey(FlowConsts.FLOW_NODE_OPTION_TIME_OUT)) {
|
||||
aiChatParams.setTimeout(oConvertUtils.getInt(metadata.getInteger(FlowConsts.FLOW_NODE_OPTION_TIME_OUT), 300));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1152,70 +964,16 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
aiChatParams.setPluginIds(pluginIds);
|
||||
}
|
||||
}
|
||||
|
||||
//流程不为空,构建插件
|
||||
if(oConvertUtils.isNotEmpty(flowId)){
|
||||
Map<String, Object> result = airagFlowPluginService.getFlowsToPlugin(flowId);
|
||||
this.addPluginToParams(aiChatParams, result);
|
||||
}
|
||||
|
||||
// 设置网络搜索参数(如果前端传递了)
|
||||
if (sendParams != null && oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) {
|
||||
aiChatParams.setEnableSearch(sendParams.getEnableSearch());
|
||||
}
|
||||
|
||||
// 设置深度思考参数(如果前端传递了)
|
||||
if (sendParams != null && oConvertUtils.isObjectNotEmpty(sendParams.getEnableThink())) {
|
||||
aiChatParams.setReturnThinking(sendParams.getEnableThink());
|
||||
}
|
||||
|
||||
// 设置记忆库的插件
|
||||
if(sendParams != null && oConvertUtils.isNotEmpty(memoryId)){
|
||||
//开启记忆
|
||||
if(null == aiApp.getIzOpenMemory() || AiAppConsts.IZ_OPEN_MEMORY.equals(aiApp.getIzOpenMemory())){
|
||||
Map<String, Object> pluginMemory = airagKnowledgeService.getPluginMemory(memoryId);
|
||||
this.addPluginToParams(aiChatParams, pluginMemory);
|
||||
}
|
||||
}
|
||||
|
||||
//设置变量的插件
|
||||
// 添加系统级工具:变量更新
|
||||
if (oConvertUtils.isNotEmpty(aiApp.getId())) {
|
||||
airagVariableService.addUpdateVariableTool(aiApp,username,aiChatParams);
|
||||
}
|
||||
|
||||
// 打印流程耗时日志
|
||||
printChatDuration(requestId, "构造应用自定义参数完成");
|
||||
// 发消息
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
sendWithDefault(requestId, chatConversation, topicId, modelId, messages, aiChatParams, sendParams.getSessionType());
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加插件到参数中
|
||||
*
|
||||
* @param aiChatParams
|
||||
* @param result
|
||||
*/
|
||||
private void addPluginToParams(AIChatParams aiChatParams, Map<String, Object> result) {
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
Map<ToolSpecification, ToolExecutor> flowsToPlugin = (Map<ToolSpecification, ToolExecutor>) result.get("pluginTool");
|
||||
String pluginId = (String) result.get("pluginId");
|
||||
if (aiChatParams.getTools() == null) {
|
||||
aiChatParams.setTools(new HashMap<>());
|
||||
}
|
||||
if (flowsToPlugin != null) {
|
||||
aiChatParams.getTools().putAll(flowsToPlugin);
|
||||
}
|
||||
if (aiChatParams.getPluginIds() == null) {
|
||||
aiChatParams.setPluginIds(new ArrayList<>());
|
||||
}
|
||||
if (oConvertUtils.isNotEmpty(pluginId)) {
|
||||
aiChatParams.getPluginIds().add(pluginId);
|
||||
}
|
||||
sendWithDefault(requestId, chatConversation, topicId, modelId, messages, aiChatParams);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1226,12 +984,11 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
* @param topicId
|
||||
* @param modelId
|
||||
* @param messages
|
||||
* @param sessionType
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 19:24
|
||||
*/
|
||||
private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId, List<ChatMessage> messages, AIChatParams aiChatParams, String sessionType) {
|
||||
private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId, List<ChatMessage> messages, AIChatParams aiChatParams) {
|
||||
// 调用ai聊天
|
||||
if (null == aiChatParams) {
|
||||
aiChatParams = new AIChatParams();
|
||||
@ -1240,16 +997,11 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
if(chatConversation.getApp().getId().equals(AiAppConsts.DEFAULT_APP_ID)){
|
||||
aiChatParams.setTools(jeecgToolsProvider.getDefaultTools());
|
||||
}
|
||||
if(CollectionUtils.isEmpty(aiChatParams.getKnowIds())){
|
||||
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
|
||||
} else {
|
||||
aiChatParams.getKnowIds().addAll(chatConversation.getApp().getKnowIds());
|
||||
}
|
||||
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
|
||||
aiChatParams.setMaxMsgNumber(oConvertUtils.getInt(chatConversation.getApp().getMsgNum(), 5));
|
||||
aiChatParams.setCurrentHttpRequest(SpringContextUtils.getHttpServletRequest());
|
||||
aiChatParams.setReturnThinking(true);
|
||||
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
|
||||
// for [QQYUN-9234] MCP服务连接关闭 - 保存参数引用用于在回调中关闭MCP连接
|
||||
final AIChatParams finalAiChatParams = aiChatParams;
|
||||
TokenStream chatStream;
|
||||
try {
|
||||
// 打印流程耗时日志
|
||||
@ -1261,8 +1013,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
// for [QQYUN-9234] MCP服务连接关闭 - 异常时关闭MCP连接
|
||||
finalAiChatParams.closeMcpConnections();
|
||||
// sse
|
||||
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
||||
if (null == emitter) {
|
||||
@ -1348,8 +1098,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
// 打印流程耗时日志
|
||||
printChatDuration(requestId, "LLM输出消息完成");
|
||||
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
|
||||
// for [QQYUN-9234] MCP服务连接关闭 - 聊天完成时关闭MCP连接
|
||||
finalAiChatParams.closeMcpConnections();
|
||||
// 记录ai的回复
|
||||
AiMessage aiMessage = responseMessage.aiMessage();
|
||||
FinishReason finishReason = responseMessage.finishReason();
|
||||
@ -1365,9 +1113,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
|
||||
appendMessage(messages, aiMessage, chatConversation, topicId);
|
||||
// 保存会话
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
saveChatConversation(chatConversation, false, httpRequest, sessionType);
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
saveChatConversation(chatConversation, false, httpRequest);
|
||||
closeSSE(emitter, eventData);
|
||||
} else if (FinishReason.LENGTH.equals(finishReason)) {
|
||||
// 上下文长度超过限制
|
||||
@ -1391,8 +1137,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
// 打印流程耗时日志
|
||||
printChatDuration(requestId, "LLM输出消息异常");
|
||||
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
|
||||
// for [QQYUN-9234] MCP服务连接关闭 - 聊天异常时关闭MCP连接
|
||||
finalAiChatParams.closeMcpConnections();
|
||||
// sse
|
||||
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
||||
if (null == emitter) {
|
||||
@ -1457,7 +1201,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
*/
|
||||
private static void sendMessage2Client(SseEmitter emitter, EventData eventData) {
|
||||
try {
|
||||
log.debug("发送消息:{}", eventData.getRequestId());
|
||||
log.info("发送消息:{}", eventData.getRequestId());
|
||||
String eventStr = JSONObject.toJSONString(eventData);
|
||||
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
|
||||
emitter.send(SseEmitter.event().data(eventStr));
|
||||
@ -1507,9 +1251,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
if (oConvertUtils.isEmpty(chatConversation.getId())) {
|
||||
return;
|
||||
}
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
String key = getConversationCacheKey(chatConversation.getId(), null,"");
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
String key = getConversationCacheKey(chatConversation.getId(), null);
|
||||
if (oConvertUtils.isEmpty(key)) {
|
||||
return;
|
||||
}
|
||||
@ -1539,13 +1281,10 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
if (oConvertUtils.isNotEmpty(summaryTitle)) {
|
||||
cachedConversation.setTitle(summaryTitle);
|
||||
} else {
|
||||
int maxLength = AiAppConsts.CONVERSATION_MAX_TITLE_LENGTH;
|
||||
cachedConversation.setTitle(question.length() > maxLength ? question.substring(0, maxLength) : question);
|
||||
cachedConversation.setTitle(question.length() > 5 ? question.substring(0, 5) : question);
|
||||
}
|
||||
//保存会话
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
saveChatConversation(cachedConversation,"");
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
saveChatConversation(cachedConversation);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -1567,7 +1306,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
} else {
|
||||
token = TokenUtils.getTokenByRequest();
|
||||
}
|
||||
if (TokenUtils.verifyToken(token, sysBaseApi, redisUtil)) {
|
||||
if (TokenUtils.verifyToken(token, sysBaseApi)) {
|
||||
return JwtUtil.getUsername(token);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@ -1590,296 +1329,4 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
log.info("[AI-CHAT]{},requestId:{},耗时:{}s", message, requestId, (System.currentTimeMillis() - beginTime) / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据会话类型获取会话信息
|
||||
*
|
||||
* @param sessionType
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public Result<?> getConversationsByType(String sessionType) {
|
||||
String key = getConversationDirCacheKey(null);
|
||||
key = key + ":" + sessionType + ":*";
|
||||
List<String> keys = redisUtil.scan(key);
|
||||
// 如果键集合为空,返回空列表
|
||||
if (keys.isEmpty()) {
|
||||
return Result.ok(Collections.emptyList());
|
||||
}
|
||||
|
||||
// 遍历键集合,获取对应的 ChatConversation 对象
|
||||
List<ChatConversation> conversations = new ArrayList<>();
|
||||
for (Object k : keys) {
|
||||
ChatConversation conversation = (ChatConversation) redisTemplate.boundValueOps(k).get();
|
||||
|
||||
if (conversation != null) {
|
||||
AiragApp app = conversation.getApp();
|
||||
if (null == app) {
|
||||
continue;
|
||||
}
|
||||
conversation.setApp(null);
|
||||
conversation.setMessages(null);
|
||||
conversations.add(conversation);
|
||||
}
|
||||
}
|
||||
|
||||
// 对会话列表按创建时间降序排序
|
||||
conversations.sort((o1, o2) -> {
|
||||
Date date1 = o1.getCreateTime();
|
||||
Date date2 = o2.getCreateTime();
|
||||
if (date1 == null && date2 == null) {
|
||||
return 0;
|
||||
}
|
||||
if (date1 == null) {
|
||||
return 1;
|
||||
}
|
||||
if (date2 == null) {
|
||||
return -1;
|
||||
}
|
||||
return date2.compareTo(date1);
|
||||
});
|
||||
|
||||
// 返回结果
|
||||
return Result.ok(conversations);
|
||||
}
|
||||
|
||||
//================================================= begin 【QQYUN-14269】【AI】支持变量 ========================================
|
||||
/**
|
||||
* 初始化变量(仅不存在时设置)
|
||||
*/
|
||||
private void saveVariables(AiragApp app) {
|
||||
if(null == app){
|
||||
return;
|
||||
}
|
||||
if(!AiAppConsts.IZ_OPEN_MEMORY.equals(app.getIzOpenMemory())){
|
||||
return;
|
||||
}
|
||||
if (oConvertUtils.isObjectNotEmpty(app.getVariables())) {
|
||||
// 变量替换
|
||||
String username = "jeecg";
|
||||
try {
|
||||
HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
|
||||
username = JwtUtil.getUserNameByToken(req);
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage());
|
||||
}
|
||||
if (oConvertUtils.isNotEmpty(username) && oConvertUtils.isNotEmpty(app.getId())) {
|
||||
String variables = app.getVariables();
|
||||
JSONArray objects = JSONArray.parseArray(variables);
|
||||
for (int i = 0; i < objects.size(); i++) {
|
||||
JSONObject jsonObject = objects.getJSONObject(i);
|
||||
String name = jsonObject.getString("name");
|
||||
String defaultValue = jsonObject.getString("defaultValue");
|
||||
if (oConvertUtils.isNotEmpty(name)) {
|
||||
airagVariableService.initVariable(username, app.getId(), name, defaultValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//================================================= end 【QQYUN-14269】【AI】支持变量 ========================================
|
||||
|
||||
/**
|
||||
* ai海报生成
|
||||
*
|
||||
* @param chatSendParams
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public String genAiPoster(ChatSendParams chatSendParams) {
|
||||
AssertUtils.assertNotEmpty("请选择绘画模型", chatSendParams.getDrawModelId());
|
||||
AssertUtils.assertNotEmpty("请填写提示词", chatSendParams.getContent());
|
||||
AIChatParams aiChatParams = new AIChatParams();
|
||||
if(oConvertUtils.isNotEmpty(chatSendParams.getImageSize())){
|
||||
aiChatParams.setImageSize(chatSendParams.getImageSize());
|
||||
}
|
||||
String image= chatSendParams.getImageUrl();
|
||||
List<Map<String, Object>> imageList = new ArrayList<>();
|
||||
if(oConvertUtils.isEmpty(image)) {
|
||||
//生成图片
|
||||
imageList = aiChatHandler.imageGenerate(chatSendParams.getDrawModelId(), chatSendParams.getContent(), aiChatParams);
|
||||
} else {
|
||||
//图生图
|
||||
imageList = aiChatHandler.imageEdit(chatSendParams.getDrawModelId(), chatSendParams.getContent(), Arrays.asList(image.split(SymbolConstant.COMMA)), aiChatParams);
|
||||
}
|
||||
return imageList.stream().map(this::uploadImage).collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传图片
|
||||
*
|
||||
* @param map
|
||||
* @return
|
||||
*/
|
||||
private String uploadImage(Map<String, Object> map) {
|
||||
if (null == map || map.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
String type = String.valueOf(map.get("type"));
|
||||
String value = String.valueOf(map.get("value"));
|
||||
byte[] data = new byte[1024];
|
||||
// 判断是否是base64
|
||||
if ("base64".equals(type)) {
|
||||
if(value.startsWith("data:image")){
|
||||
value = value.substring(value.indexOf(",") + 1);
|
||||
}
|
||||
data = Base64.getDecoder().decode(value);
|
||||
} else {
|
||||
//下载网络图片
|
||||
InputStream inputStream = FileDownloadUtils.getDownInputStream(value, "");
|
||||
if (inputStream != null) {
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
byte[] inpByte = new byte[1024]; // 1KB缓冲区
|
||||
int nRead;
|
||||
while ((nRead = inputStream.read(inpByte, 0, data.length)) != -1) {
|
||||
buffer.write(inpByte, 0, nRead);
|
||||
}
|
||||
data = buffer.toByteArray();
|
||||
}
|
||||
}
|
||||
if (data != null) {
|
||||
Path path = jeecgBaseConfig.getPath();
|
||||
String bizPath = "chat";
|
||||
String url = CommonUtils.uploadOnlineImage(data, path.getUpload(), bizPath, jeecgBaseConfig.getUploadType());
|
||||
if("local".equals(jeecgBaseConfig.getUploadType())){
|
||||
url = "#{domainURL}/" + url;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("上传图片失败", e);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
//================================================= begin【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档========================================
|
||||
/**
|
||||
* 构建文件内容
|
||||
*
|
||||
* @param content
|
||||
* @param files
|
||||
* @return
|
||||
*/
|
||||
private String buildContentWithFiles(String content, List<String> files) {
|
||||
String filesText = parseFilesToText(files);
|
||||
if (oConvertUtils.isEmpty(content)) {
|
||||
content = "请基于我提供的附件内容回答问题。";
|
||||
}else{
|
||||
content = content + "\n\n请基于我提供的附件内容回答问题。";
|
||||
}
|
||||
if (oConvertUtils.isNotEmpty(filesText)) {
|
||||
if (oConvertUtils.isNotEmpty(content)) {
|
||||
content = content + "\n\n" + filesText;
|
||||
} else {
|
||||
content = filesText;
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将文件转换成text
|
||||
*
|
||||
* @param files
|
||||
* @return
|
||||
*/
|
||||
private String parseFilesToText(List<String> files) {
|
||||
if (com.baomidou.mybatisplus.core.toolkit.CollectionUtils.isEmpty(files)) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
TikaDocumentParser parser = new TikaDocumentParser(AutoDetectParser::new, null, null, null);
|
||||
int parsedCount = 0;
|
||||
for (String fileRef : files) {
|
||||
if (parsedCount >= LLMConsts.CHAT_FILE_MAX_COUNT) {
|
||||
break;
|
||||
}
|
||||
if (oConvertUtils.isEmpty(fileRef)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String fileRefWithoutQuery = fileRef;
|
||||
if (fileRefWithoutQuery.contains("?")) {
|
||||
fileRefWithoutQuery = fileRefWithoutQuery.substring(0, fileRefWithoutQuery.indexOf("?"));
|
||||
}
|
||||
String fileName = FilenameUtils.getName(fileRefWithoutQuery);
|
||||
String ext = FilenameUtils.getExtension(fileName);
|
||||
if (oConvertUtils.isEmpty(ext) || !LLMConsts.CHAT_FILE_EXT_WHITELIST.contains(ext.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
File file = ensureLocalFile(fileRef, fileName);
|
||||
if (file == null || !file.exists() || !file.isFile()) {
|
||||
continue;
|
||||
}
|
||||
Document document = parser.parse(file);
|
||||
if (document == null || oConvertUtils.isEmpty(document.text())) {
|
||||
continue;
|
||||
}
|
||||
String text = document.text().trim();
|
||||
if (text.length() > LLMConsts.CHAT_FILE_TEXT_MAX_LENGTH) {
|
||||
text = text.substring(0, LLMConsts.CHAT_FILE_TEXT_MAX_LENGTH);
|
||||
}
|
||||
sb.append("附件[").append(fileName).append("]内容:\n").append(text).append("\n\n");
|
||||
parsedCount++;
|
||||
if (sb.length() > LLMConsts.CHAT_FILE_TEXT_MAX_LENGTH) {
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("附件解析失败: {}, {}", fileRef, e.getMessage());
|
||||
}
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本地文件
|
||||
*
|
||||
* @param fileRef
|
||||
* @param fileName
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
private File ensureLocalFile(String fileRef, String fileName) {
|
||||
String uploadpath = jeecgBaseConfig.getPath().getUpload();
|
||||
if (LLMConsts.WEB_PATTERN.matcher(fileRef).matches()) {
|
||||
String tempDir = uploadpath + File.separator + "chat" + File.separator + UUID.randomUUID() + File.separator;
|
||||
File dir = new File(tempDir);
|
||||
if (!dir.exists() && !dir.mkdirs()) {
|
||||
return null;
|
||||
}
|
||||
String tempFilePath = tempDir + fileName;
|
||||
FileDownloadUtils.download2DiskFromNet(fileRef, tempFilePath);
|
||||
return new File(tempFilePath);
|
||||
}
|
||||
return new File(uploadpath + File.separator + fileRef);
|
||||
}
|
||||
//================================================= end【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档========================================
|
||||
|
||||
|
||||
/**
|
||||
* ai创作
|
||||
*
|
||||
* @param aiWriteGenerateVo
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public SseEmitter genAiWriter(AiWriteGenerateVo aiWriteGenerateVo) {
|
||||
String activeMode = "compose";
|
||||
ChatSendParams sendParams = new ChatSendParams();
|
||||
sendParams.setAppId(AiAppConsts.WRITER_APP_ID);
|
||||
String content = "";
|
||||
//写作
|
||||
if (activeMode.equals(aiWriteGenerateVo.getActiveMode())) {
|
||||
content = StrUtil.format(Prompts.AI_WRITER_PROMPT, aiWriteGenerateVo.getPrompt(), aiWriteGenerateVo.getFormat(), aiWriteGenerateVo.getTone(), aiWriteGenerateVo.getLanguage(), aiWriteGenerateVo.getLength());
|
||||
} else {
|
||||
//回复
|
||||
content = StrUtil.format(Prompts.AI_REPLY_PROMPT, aiWriteGenerateVo.getPrompt(), aiWriteGenerateVo.getOriginalContent(), aiWriteGenerateVo.getFormat(), aiWriteGenerateVo.getTone(), aiWriteGenerateVo.getLanguage(), aiWriteGenerateVo.getLength());
|
||||
}
|
||||
sendParams.setContent(content);
|
||||
sendParams.setIzSaveSession(false);
|
||||
return this.send(sendParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,194 +0,0 @@
|
||||
package org.jeecg.modules.airag.app.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
|
||||
import dev.langchain4j.service.tool.ToolExecutor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
import org.jeecg.modules.airag.app.service.IAiragVariableService;
|
||||
import org.jeecg.modules.airag.app.vo.AppVariableVo;
|
||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: AI应用变量服务实现
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-26
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AiragVariableServiceImpl implements IAiragVariableService {
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate redisTemplate;
|
||||
|
||||
private static final String CACHE_PREFIX = "airag:app:var:";
|
||||
|
||||
/**
|
||||
* 初始化变量(仅不存在时设置)
|
||||
*
|
||||
* @param username
|
||||
* @param appId
|
||||
* @param name
|
||||
* @param defaultValue
|
||||
*/
|
||||
@Override
|
||||
public void initVariable(String username, String appId, String name, String defaultValue) {
|
||||
if (oConvertUtils.isEmpty(username) || oConvertUtils.isEmpty(appId) || oConvertUtils.isEmpty(name)) {
|
||||
return;
|
||||
}
|
||||
String key = CACHE_PREFIX + appId + ":" + username;
|
||||
redisTemplate.opsForHash().putIfAbsent(key, name, defaultValue != null ? defaultValue : "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加提示词
|
||||
*
|
||||
* @param username
|
||||
* @param app
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public String additionalPrompt(String username, AiragApp app) {
|
||||
String memoryPrompt = app.getMemoryPrompt();
|
||||
String prompt = app.getPrompt();
|
||||
|
||||
if (oConvertUtils.isEmpty(memoryPrompt)) {
|
||||
return prompt;
|
||||
}
|
||||
String variablesStr = app.getVariables();
|
||||
if (oConvertUtils.isEmpty(variablesStr)) {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
List<AppVariableVo> variableList = JSONArray.parseArray(variablesStr, AppVariableVo.class);
|
||||
if (variableList == null || variableList.isEmpty()) {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
String key = CACHE_PREFIX + app.getId() + ":" + username;
|
||||
Map<Object, Object> savedValues = redisTemplate.opsForHash().entries(key);
|
||||
|
||||
for (AppVariableVo variable : variableList) {
|
||||
if (variable.getEnable() != null && !variable.getEnable()) {
|
||||
continue;
|
||||
}
|
||||
String name = variable.getName();
|
||||
String value = variable.getDefaultValue();
|
||||
|
||||
// 优先使用Redis中的值
|
||||
if (savedValues.containsKey(name)) {
|
||||
Object savedVal = savedValues.get(name);
|
||||
if (savedVal != null) {
|
||||
value = String.valueOf(savedVal);
|
||||
}
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
value = "";
|
||||
}
|
||||
|
||||
// 替换 {{name}}
|
||||
memoryPrompt = memoryPrompt.replace("{{" + name + "}}", value);
|
||||
}
|
||||
return prompt + "\n" + memoryPrompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新变量值
|
||||
*
|
||||
* @param userId
|
||||
* @param appId
|
||||
* @param name
|
||||
* @param value
|
||||
*/
|
||||
@Override
|
||||
public void updateVariable(String userId, String appId, String name, String value) {
|
||||
if (oConvertUtils.isEmpty(userId) || oConvertUtils.isEmpty(appId) || oConvertUtils.isEmpty(name)) {
|
||||
return;
|
||||
}
|
||||
String key = CACHE_PREFIX + appId + ":" + userId;
|
||||
redisTemplate.opsForHash().put(key, name, value);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 添加变量更新工具
|
||||
*
|
||||
* @param params
|
||||
* @param aiApp
|
||||
* @param username
|
||||
*/
|
||||
@Override
|
||||
public void addUpdateVariableTool(AiragApp aiApp, String username, AIChatParams params) {
|
||||
if (params.getTools() == null) {
|
||||
params.setTools(new HashMap<>());
|
||||
}
|
||||
if (!AiAppConsts.IZ_OPEN_MEMORY.equals(aiApp.getIzOpenMemory())) {
|
||||
return;
|
||||
}
|
||||
// 构建变量描述信息
|
||||
String variablesStr = aiApp.getVariables();
|
||||
List<AppVariableVo> variableList = null;
|
||||
if (oConvertUtils.isNotEmpty(variablesStr)) {
|
||||
variableList = JSONArray.parseArray(variablesStr, AppVariableVo.class);
|
||||
}
|
||||
|
||||
//工具描述
|
||||
StringBuilder descriptionBuilder = new StringBuilder("更新应用变量的值。仅当检测到变量的新值与当前值不一致时调用。如果已调用过或值未变,请勿重复调用。");
|
||||
if (variableList != null && !variableList.isEmpty()) {
|
||||
descriptionBuilder.append("\n\n可用变量列表:");
|
||||
for (AppVariableVo var : variableList) {
|
||||
if (var.getEnable() != null && !var.getEnable()) {
|
||||
continue;
|
||||
}
|
||||
descriptionBuilder.append("\n- ").append(var.getName());
|
||||
if (oConvertUtils.isNotEmpty(var.getDescription())) {
|
||||
descriptionBuilder.append(": ").append(var.getDescription());
|
||||
}
|
||||
}
|
||||
descriptionBuilder.append("\n\n注意:variableName必须是上述列表中的名称之一。");
|
||||
}
|
||||
|
||||
//构建更新变量的工具
|
||||
ToolSpecification spec = ToolSpecification.builder()
|
||||
.name("update_variable")
|
||||
.description(descriptionBuilder.toString())
|
||||
.parameters(JsonObjectSchema.builder()
|
||||
.addStringProperty("variableName", "变量名称")
|
||||
.addStringProperty("value", "变量值")
|
||||
.required("variableName", "value")
|
||||
.build())
|
||||
.build();
|
||||
|
||||
//监听工具的调用
|
||||
ToolExecutor executor = (toolExecutionRequest, memoryId) -> {
|
||||
try {
|
||||
JSONObject args = JSONObject.parseObject(toolExecutionRequest.arguments());
|
||||
String name = args.getString("variableName");
|
||||
String value = args.getString("value");
|
||||
IAiragVariableService variableService = SpringContextUtils.getBean(IAiragVariableService.class);
|
||||
//更新变量值
|
||||
variableService.updateVariable(username, aiApp.getId(), name, value);
|
||||
return "变量 " + name + " 已更新为: " + value;
|
||||
} catch (Exception e) {
|
||||
log.error("更新变量失败", e);
|
||||
return "更新变量失败: " + e.getMessage();
|
||||
}
|
||||
};
|
||||
|
||||
params.getTools().put(spec, executor);
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
package org.jeecg.modules.airag.app.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @Description: ai写作生成实体类
|
||||
*
|
||||
* @author: wangshuai
|
||||
* @date: 2026/1/12 15:59
|
||||
*/
|
||||
@Data
|
||||
public class AiWriteGenerateVo {
|
||||
|
||||
/**
|
||||
* 写作类型
|
||||
*/
|
||||
private String activeMode;
|
||||
|
||||
/**
|
||||
* 写作内容提示
|
||||
*/
|
||||
private String prompt;
|
||||
|
||||
/**
|
||||
* 原文
|
||||
*/
|
||||
private String originalContent;
|
||||
|
||||
/**
|
||||
* 长度
|
||||
*/
|
||||
private String length;
|
||||
|
||||
/**
|
||||
* 格式
|
||||
*/
|
||||
private String format;
|
||||
|
||||
/**
|
||||
* 语气
|
||||
*/
|
||||
private String tone;
|
||||
|
||||
/**
|
||||
* 语言
|
||||
*/
|
||||
private String language;
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
package org.jeecg.modules.airag.app.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @Description: 应用变量配置
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-26
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
public class AppVariableVo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 变量名
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 默认值
|
||||
*/
|
||||
private String defaultValue;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
private Boolean enable;
|
||||
|
||||
/**
|
||||
* 动作
|
||||
*/
|
||||
private String action;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
private Integer orderNum;
|
||||
}
|
||||
@ -47,14 +47,4 @@ public class ChatConversation {
|
||||
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
|
||||
*/
|
||||
private Map<String, Object> flowInputs;
|
||||
|
||||
/**
|
||||
* portal 应用门户
|
||||
*/
|
||||
private String sessionType;
|
||||
|
||||
/**
|
||||
* 是否保存会话
|
||||
*/
|
||||
private Boolean izSaveSession;
|
||||
}
|
||||
|
||||
@ -47,11 +47,6 @@ public class ChatSendParams {
|
||||
*/
|
||||
private List<String> images;
|
||||
|
||||
/**
|
||||
* 文件列表
|
||||
*/
|
||||
private List<String> files;
|
||||
|
||||
/**
|
||||
* 工作流额外入参配置
|
||||
* key: 参数field, value: 参数值
|
||||
@ -64,39 +59,4 @@ public class ChatSendParams {
|
||||
*/
|
||||
private Boolean enableSearch;
|
||||
|
||||
/**
|
||||
* 是否开启深度思考
|
||||
*/
|
||||
private Boolean enableThink;
|
||||
|
||||
/**
|
||||
* 会话类型: portal 应用门户
|
||||
*/
|
||||
private String sessionType;
|
||||
|
||||
/**
|
||||
* 是否开启生成绘画
|
||||
*/
|
||||
private Boolean enableDraw;
|
||||
|
||||
/**
|
||||
* 绘画模型的id
|
||||
*/
|
||||
private String drawModelId;
|
||||
|
||||
/**
|
||||
* 图片尺寸
|
||||
*/
|
||||
private String imageSize;
|
||||
|
||||
/**
|
||||
* 一张图片
|
||||
*/
|
||||
private String imageUrl;
|
||||
|
||||
/**
|
||||
* 是否保存会话
|
||||
*/
|
||||
private Boolean izSaveSession;
|
||||
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package org.jeecg.modules.airag.demo;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@ -12,15 +11,12 @@ import java.util.Map;
|
||||
* @Author: chenrui
|
||||
* @Date: 2025/3/6 11:42
|
||||
*/
|
||||
@Slf4j
|
||||
@Component("testAiragEnhance")
|
||||
public class TestAiragEnhance implements IAiRagEnhanceJava {
|
||||
@Override
|
||||
public Map<String, Object> process(Map<String, Object> inputParams) {
|
||||
Object arg1 = inputParams.get("arg1");
|
||||
Object arg2 = inputParams.get("arg2");
|
||||
Object index = inputParams.get("index");
|
||||
log.info("arg1={}, arg2={}, index={}", arg1, arg2, index);
|
||||
return Collections.singletonMap("result",arg1.toString()+"java拼接"+arg2.toString());
|
||||
}
|
||||
}
|
||||
@ -1,209 +0,0 @@
|
||||
package org.jeecg.modules.airag.llm.consts;
|
||||
|
||||
/**
|
||||
* @Description: 流程插件常量
|
||||
*
|
||||
* @author: wangshuai
|
||||
* @date: 2025/12/23 19:37
|
||||
*/
|
||||
public interface FlowPluginContent {
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
String NAME = "name";
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
String DESCRIPTION = "description";
|
||||
|
||||
/**
|
||||
* 响应
|
||||
*/
|
||||
String RESPONSES = "responses";
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
String TYPE = "type";
|
||||
|
||||
/**
|
||||
* 参数
|
||||
*/
|
||||
String PARAMETERS = "parameters";
|
||||
|
||||
/**
|
||||
* 是否必须
|
||||
*/
|
||||
String REQUIRED = "required";
|
||||
|
||||
/**
|
||||
* 默认值
|
||||
*/
|
||||
String DEFAULT_VALUE = "defaultValue";
|
||||
|
||||
/**
|
||||
* 路径
|
||||
*/
|
||||
String PATH = "path";
|
||||
|
||||
/**
|
||||
* 方法
|
||||
*/
|
||||
String METHOD = "method";
|
||||
|
||||
/**
|
||||
* 位置
|
||||
*/
|
||||
String LOCATION = "location";
|
||||
|
||||
/**
|
||||
* 认证类型
|
||||
*/
|
||||
String AUTH_TYPE = "authType";
|
||||
|
||||
/**
|
||||
* token参数名称
|
||||
*/
|
||||
String TOKEN_PARAM_NAME = "tokenParamName";
|
||||
|
||||
/**
|
||||
* token参数值
|
||||
*/
|
||||
String TOKEN_PARAM_VALUE = "tokenParamValue";
|
||||
|
||||
/**
|
||||
* token
|
||||
*/
|
||||
String TOKEN = "token";
|
||||
|
||||
/**
|
||||
* Path位置
|
||||
*/
|
||||
String LOCATION_PATH = "Path";
|
||||
|
||||
/**
|
||||
* Header位置
|
||||
*/
|
||||
String LOCATION_HEADER = "Header";
|
||||
|
||||
/**
|
||||
* Query位置
|
||||
*/
|
||||
String LOCATION_QUERY = "Query";
|
||||
|
||||
/**
|
||||
* Body位置
|
||||
*/
|
||||
String LOCATION_BODY = "Body";
|
||||
|
||||
/**
|
||||
* Form-Data位置
|
||||
*/
|
||||
String LOCATION_FORM_DATA = "Form-Data";
|
||||
|
||||
/**
|
||||
* String类型
|
||||
*/
|
||||
String TYPE_STRING = "String";
|
||||
|
||||
/**
|
||||
* string类型
|
||||
*/
|
||||
String TYPE_STRING_LOWER = "string";
|
||||
|
||||
/**
|
||||
* Number类型
|
||||
*/
|
||||
String TYPE_NUMBER = "Number";
|
||||
|
||||
/**
|
||||
* number类型
|
||||
*/
|
||||
String TYPE_NUMBER_LOWER = "number";
|
||||
|
||||
/**
|
||||
* Integer类型
|
||||
*/
|
||||
String TYPE_INTEGER = "Integer";
|
||||
|
||||
/**
|
||||
* integer类型
|
||||
*/
|
||||
String TYPE_INTEGER_LOWER = "integer";
|
||||
|
||||
/**
|
||||
* Boolean类型
|
||||
*/
|
||||
String TYPE_BOOLEAN = "Boolean";
|
||||
|
||||
/**
|
||||
* boolean类型
|
||||
*/
|
||||
String TYPE_BOOLEAN_LOWER = "boolean";
|
||||
|
||||
/**
|
||||
* 工具数量
|
||||
*/
|
||||
String TOOL_COUNT = "tool_count";
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
String ENABLED = "enabled";
|
||||
|
||||
/**
|
||||
* 输入
|
||||
*/
|
||||
String INPUTS = "inputs";
|
||||
|
||||
/**
|
||||
* 输出
|
||||
*/
|
||||
String OUTPUTS = "outputs";
|
||||
|
||||
/**
|
||||
* POST请求
|
||||
*/
|
||||
String POST = "POST";
|
||||
|
||||
/**
|
||||
* token名称
|
||||
*/
|
||||
String X_ACCESS_TOKEN = "X-Access-Token";
|
||||
|
||||
/**
|
||||
* 插件名称
|
||||
*/
|
||||
String PLUGIN_NAME = "流程调用";
|
||||
|
||||
/**
|
||||
* 插件描述
|
||||
*/
|
||||
String PLUGIN_DESC = "调用工作流";
|
||||
|
||||
/**
|
||||
* 插件请求地址
|
||||
*/
|
||||
String PLUGIN_REQUEST_URL = "/airag/flow/plugin/run/";
|
||||
|
||||
/**
|
||||
* 记忆库插件名称
|
||||
*/
|
||||
String PLUGIN_MEMORY_NAME = "记忆库";
|
||||
|
||||
/**
|
||||
* 记忆库插件描述
|
||||
*/
|
||||
String PLUGIN_MEMORY_DESC = "用于记录长期记忆";
|
||||
|
||||
/**
|
||||
* 添加记忆路径
|
||||
*/
|
||||
String PLUGIN_MEMORY_ADD_PATH = "/airag/knowledge/plugin/add";
|
||||
|
||||
/**
|
||||
* 查询记忆路径
|
||||
*/
|
||||
String PLUGIN_MEMORY_QUERY_PATH = "/airag/knowledge/plugin/query";
|
||||
}
|
||||
@ -1,8 +1,5 @@
|
||||
package org.jeecg.modules.airag.llm.consts;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
@ -38,11 +35,6 @@ public class LLMConsts {
|
||||
*/
|
||||
public static final String MODEL_TYPE_LLM = "LLM";
|
||||
|
||||
/**
|
||||
* 模型类型: 图像生成
|
||||
*/
|
||||
public static final String MODEL_TYPE_IMAGE = "IMAGE";
|
||||
|
||||
/**
|
||||
* 向量模型:默认维度
|
||||
*/
|
||||
@ -93,29 +85,4 @@ public class LLMConsts {
|
||||
*/
|
||||
public static final String DEEPSEEK_REASONER = "deepseek-reasoner";
|
||||
|
||||
/**
|
||||
* 知识库类型:知识库
|
||||
*/
|
||||
public static final String KNOWLEDGE_TYPE_KNOWLEDGE = "knowledge";
|
||||
|
||||
/**
|
||||
* 知识库类型:记忆库
|
||||
*/
|
||||
public static final String KNOWLEDGE_TYPE_MEMORY = "memory";
|
||||
|
||||
/**
|
||||
* 支持文件的后缀
|
||||
*/
|
||||
public static final Set<String> CHAT_FILE_EXT_WHITELIST = new HashSet<>(Arrays.asList("txt", "pdf", "docx", "doc", "pptx", "ppt", "xlsx", "xls", "md"));
|
||||
|
||||
/**
|
||||
* 文件内容最大长度
|
||||
*/
|
||||
public static final int CHAT_FILE_TEXT_MAX_LENGTH = 20000;
|
||||
|
||||
/**
|
||||
* 上传文件对打数量
|
||||
*/
|
||||
public static final int CHAT_FILE_MAX_COUNT = 3;
|
||||
|
||||
}
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
package org.jeecg.modules.airag.llm.controller;
|
||||
|
||||
import org.jeecg.common.airag.api.IAiragBaseApi;
|
||||
import org.jeecg.modules.airag.llm.service.impl.AiragBaseApiImpl;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* airag baseAPI Controller
|
||||
*
|
||||
* @author sjlei
|
||||
* @date 2025-12-30
|
||||
*/
|
||||
@RestController("airagBaseApiController")
|
||||
public class AiragBaseApiController implements IAiragBaseApi {
|
||||
|
||||
@Autowired
|
||||
AiragBaseApiImpl airagBaseApi;
|
||||
|
||||
@PostMapping("/airag/api/knowledgeWriteTextDocument")
|
||||
public String knowledgeWriteTextDocument(
|
||||
@RequestParam("knowledgeId") String knowledgeId,
|
||||
@RequestParam("title") String title,
|
||||
@RequestParam("content") String content
|
||||
) {
|
||||
return airagBaseApi.knowledgeWriteTextDocument(knowledgeId, title, content);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,18 +1,14 @@
|
||||
package org.jeecg.modules.airag.llm.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.TokenUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
|
||||
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||
@ -26,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@ -80,12 +77,9 @@ public class AiragKnowledgeController {
|
||||
* @date 2025/2/18 17:09
|
||||
*/
|
||||
@PostMapping(value = "/add")
|
||||
@RequiresPermissions("airag:knowledge:add")
|
||||
@SaCheckPermission("airag:knowledge:add")
|
||||
public Result<String> add(@RequestBody AiragKnowledge airagKnowledge) {
|
||||
airagKnowledge.setStatus(LLMConsts.STATUS_ENABLE);
|
||||
if(oConvertUtils.isEmpty(airagKnowledge.getType())) {
|
||||
airagKnowledge.setType(LLMConsts.KNOWLEDGE_TYPE_KNOWLEDGE);
|
||||
}
|
||||
airagKnowledgeService.save(airagKnowledge);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
@ -100,16 +94,13 @@ public class AiragKnowledgeController {
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
@RequiresPermissions("airag:knowledge:edit")
|
||||
@SaCheckPermission("airag:knowledge:edit")
|
||||
public Result<String> edit(@RequestBody AiragKnowledge airagKnowledge) {
|
||||
AiragKnowledge airagKnowledgeEntity = airagKnowledgeService.getById(airagKnowledge.getId());
|
||||
if (airagKnowledgeEntity == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
String oldEmbedId = airagKnowledgeEntity.getEmbedId();
|
||||
if(oConvertUtils.isEmpty(airagKnowledgeEntity.getType())) {
|
||||
airagKnowledge.setType(LLMConsts.KNOWLEDGE_TYPE_KNOWLEDGE);
|
||||
}
|
||||
airagKnowledgeService.updateById(airagKnowledge);
|
||||
if (!oldEmbedId.equalsIgnoreCase(airagKnowledge.getEmbedId())) {
|
||||
// 更新了模型,重建文档
|
||||
@ -127,7 +118,7 @@ public class AiragKnowledgeController {
|
||||
* @date 2025/3/12 17:05
|
||||
*/
|
||||
@PutMapping(value = "/rebuild")
|
||||
@RequiresPermissions("airag:knowledge:rebuild")
|
||||
@SaCheckPermission("airag:knowledge:rebuild")
|
||||
public Result<?> rebuild(@RequestParam("knowIds") String knowIds) {
|
||||
String[] knowIdArr = knowIds.split(",");
|
||||
for (String knowId : knowIdArr) {
|
||||
@ -146,7 +137,7 @@ public class AiragKnowledgeController {
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@DeleteMapping(value = "/delete")
|
||||
@RequiresPermissions("airag:knowledge:delete")
|
||||
@SaCheckPermission("airag:knowledge:delete")
|
||||
public Result<String> delete(HttpServletRequest request, @RequestParam(name = "id", required = true) String id) {
|
||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
||||
@ -213,7 +204,7 @@ public class AiragKnowledgeController {
|
||||
* @date 2025/2/18 15:47
|
||||
*/
|
||||
@PostMapping(value = "/doc/edit")
|
||||
@RequiresPermissions("airag:knowledge:doc:edit")
|
||||
@SaCheckPermission("airag:knowledge:doc:edit")
|
||||
public Result<?> addDocument(@RequestBody AiragKnowledgeDoc airagKnowledgeDoc) {
|
||||
return airagKnowledgeDocService.editDocument(airagKnowledgeDoc);
|
||||
}
|
||||
@ -226,7 +217,7 @@ public class AiragKnowledgeController {
|
||||
* @date 2025/3/20 11:29
|
||||
*/
|
||||
@PostMapping(value = "/doc/import/zip")
|
||||
@RequiresPermissions("airag:knowledge:doc:zip")
|
||||
@SaCheckPermission("airag:knowledge:doc:zip")
|
||||
public Result<?> importDocumentFromZip(@RequestParam(name = "knowId", required = true) String knowId,
|
||||
@RequestParam(name = "file", required = true) MultipartFile file) {
|
||||
return airagKnowledgeDocService.importDocumentFromZip(knowId,file);
|
||||
@ -253,7 +244,7 @@ public class AiragKnowledgeController {
|
||||
* @date 2025/2/18 15:47
|
||||
*/
|
||||
@PutMapping(value = "/doc/rebuild")
|
||||
@RequiresPermissions("airag:knowledge:doc:rebuild")
|
||||
@SaCheckPermission("airag:knowledge:doc:rebuild")
|
||||
public Result<?> rebuildDocument(@RequestParam("docIds") String docIds) {
|
||||
return airagKnowledgeDocService.rebuildDocument(docIds);
|
||||
}
|
||||
@ -268,7 +259,7 @@ public class AiragKnowledgeController {
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@DeleteMapping(value = "/doc/deleteBatch")
|
||||
@RequiresPermissions("airag:knowledge:doc:deleteBatch")
|
||||
@SaCheckPermission("airag:knowledge:doc:deleteBatch")
|
||||
public Result<String> deleteDocumentBatch(HttpServletRequest request, @RequestParam(name = "ids", required = true) String ids) {
|
||||
List<String> idsList = Arrays.asList(ids.split(","));
|
||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
@ -296,7 +287,7 @@ public class AiragKnowledgeController {
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@DeleteMapping(value = "/doc/deleteAll")
|
||||
@RequiresPermissions("airag:knowledge:doc:deleteAll")
|
||||
@SaCheckPermission("airag:knowledge:doc:deleteAll")
|
||||
public Result<?> deleteDocumentAll(HttpServletRequest request, @RequestParam(name = "knowId") String knowId) {
|
||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
||||
@ -366,62 +357,5 @@ public class AiragKnowledgeController {
|
||||
List<AiragKnowledge> airagKnowledges = airagKnowledgeService.listByIds(idList);
|
||||
return Result.OK(airagKnowledges);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加记忆
|
||||
*
|
||||
* @param airagKnowledgeDoc
|
||||
* @return
|
||||
*/
|
||||
@Operation(summary = "添加记忆")
|
||||
@PostMapping(value = "/plugin/add")
|
||||
public Result<?> add(@RequestBody AiragKnowledgeDoc airagKnowledgeDoc, HttpServletRequest request) {
|
||||
if (oConvertUtils.isEmpty(airagKnowledgeDoc.getKnowledgeId())) {
|
||||
return Result.error("知识库ID不能为空");
|
||||
}
|
||||
if (oConvertUtils.isEmpty(airagKnowledgeDoc.getContent())) {
|
||||
return Result.error("内容不能为空");
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if (oConvertUtils.isEmpty(airagKnowledgeDoc.getTitle())) {
|
||||
// 取内容前20个字作为标题
|
||||
String content = airagKnowledgeDoc.getContent();
|
||||
String title = content.length() > 20 ? content.substring(0, 20) : content;
|
||||
airagKnowledgeDoc.setTitle(title);
|
||||
}
|
||||
|
||||
airagKnowledgeDoc.setType(LLMConsts.KNOWLEDGE_DOC_TYPE_TEXT);
|
||||
// 保存并构建向量
|
||||
return airagKnowledgeDocService.editDocument(airagKnowledgeDoc);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询记忆
|
||||
*
|
||||
* @param params
|
||||
* @return
|
||||
*/
|
||||
@Operation(summary = "查询记忆")
|
||||
@PostMapping(value = "/plugin/query")
|
||||
public Result<?> pluginQuery(@RequestBody Map<String, Object> params, HttpServletRequest request) {
|
||||
String knowId = (String) params.get("knowledgeId");
|
||||
String queryText = (String) params.get("queryText");
|
||||
if (oConvertUtils.isEmpty(knowId)) {
|
||||
return Result.error("知识库ID不能为空");
|
||||
}
|
||||
if (oConvertUtils.isEmpty(queryText)) {
|
||||
return Result.error("查询内容不能为空");
|
||||
}
|
||||
LambdaQueryWrapper<AiragKnowledgeDoc> queryWrapper = new LambdaQueryWrapper<AiragKnowledgeDoc>();
|
||||
queryWrapper.eq(AiragKnowledgeDoc::getKnowledgeId, knowId);
|
||||
long count = airagKnowledgeDocService.count(queryWrapper);
|
||||
if(count == 0){
|
||||
return Result.ok("");
|
||||
}
|
||||
// 默认查询前5条
|
||||
KnowledgeSearchResult searchResp = embeddingHandler.embeddingSearch(Collections.singletonList(knowId), queryText, (int) count, null);
|
||||
return Result.ok(searchResp);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -169,7 +169,7 @@ public class AiragMcpController extends JeecgController<AiragMcp, IAiragMcpServi
|
||||
* @param request
|
||||
* @param airagMcp
|
||||
*/
|
||||
// @RequiresPermissions("llm:airag_mcp:exportXls")
|
||||
// @SaCheckPermission("llm:airag_mcp:exportXls")
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, AiragMcp airagMcp) {
|
||||
return super.exportXls(request, airagMcp, AiragMcp.class, "MCP");
|
||||
@ -182,7 +182,7 @@ public class AiragMcpController extends JeecgController<AiragMcp, IAiragMcpServi
|
||||
* @param response
|
||||
* @return
|
||||
*/
|
||||
// @RequiresPermissions("llm:airag_mcp:importExcel")
|
||||
// @SaCheckPermission("llm:airag_mcp:importExcel")
|
||||
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
|
||||
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
|
||||
return super.importExcel(request, response, AiragMcp.class);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package org.jeecg.modules.airag.llm.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
@ -7,7 +8,6 @@ import dev.langchain4j.data.message.UserMessage;
|
||||
import dev.langchain4j.model.embedding.EmbeddingModel;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.ai.factory.AiModelFactory;
|
||||
import org.jeecg.ai.factory.AiModelOptions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
@ -17,7 +17,6 @@ import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.TokenUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||
import org.jeecg.modules.airag.llm.handler.AIChatHandler;
|
||||
@ -73,7 +72,7 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
|
||||
* @return
|
||||
*/
|
||||
@PostMapping(value = "/add")
|
||||
@RequiresPermissions("airag:model:add")
|
||||
@SaCheckPermission("airag:model:add")
|
||||
public Result<String> add(@RequestBody AiragModel airagModel) {
|
||||
// 验证 模型名称/模型类型/基础模型
|
||||
AssertUtils.assertNotEmpty("模型名称不能为空", airagModel.getName());
|
||||
@ -96,7 +95,7 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
@RequiresPermissions("airag:model:edit")
|
||||
@SaCheckPermission("airag:model:edit")
|
||||
public Result<String> edit(@RequestBody AiragModel airagModel) {
|
||||
airagModelService.updateById(airagModel);
|
||||
return Result.OK("编辑成功!");
|
||||
@ -109,7 +108,7 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
|
||||
* @return
|
||||
*/
|
||||
@DeleteMapping(value = "/delete")
|
||||
@RequiresPermissions("airag:model:delete")
|
||||
@SaCheckPermission("airag:model:delete")
|
||||
public Result<String> delete(HttpServletRequest request, @RequestParam(name = "id", required = true) String id) {
|
||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
||||
@ -173,16 +172,11 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
|
||||
try {
|
||||
if(LLMConsts.MODEL_TYPE_LLM.equals(airagModel.getModelType())){
|
||||
aiChatHandler.completions(airagModel, Collections.singletonList(UserMessage.from("To test whether it can be successfully called, simply return success")), null);
|
||||
}else if(LLMConsts.MODEL_TYPE_EMBED.equals(airagModel.getModelType())){
|
||||
}else{
|
||||
AiModelOptions aiModelOptions = EmbeddingHandler.buildModelOptions(airagModel);
|
||||
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(aiModelOptions);
|
||||
embeddingModel.embed("test text");
|
||||
//update-begin---author:wangshuai---date:2026-01-07---for:【QQYUN-12145】【AI】AI 绘画创作---=
|
||||
}else if(LLMConsts.MODEL_TYPE_IMAGE.equals(airagModel.getModelType())){
|
||||
AIChatParams aiChatParams = new AIChatParams();
|
||||
aiChatHandler.imageGenerate(airagModel, "To test whether it can be successfully called, simply return success", aiChatParams);
|
||||
}
|
||||
//update-end---author:wangshuai---date:2026-01-07---for:【QQYUN-12145】【AI】AI 绘画创作---
|
||||
}catch (Exception e){
|
||||
log.error("测试模型连接失败", e);
|
||||
return Result.error("测试模型连接失败,请检查模型配置是否正确!");
|
||||
|
||||
@ -7,7 +7,6 @@ package org.jeecg.modules.airag.llm.document;
|
||||
|
||||
import dev.langchain4j.data.document.BlankDocumentException;
|
||||
import dev.langchain4j.data.document.Document;
|
||||
import dev.langchain4j.data.document.parser.apache.poi.ApachePoiDocumentParser;
|
||||
import dev.langchain4j.internal.Utils;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.poi.hslf.usermodel.HSLFTextParagraph;
|
||||
@ -31,10 +30,7 @@ import org.xml.sax.ContentHandler;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -55,8 +51,6 @@ public class TikaDocumentParser {
|
||||
private final Supplier<ContentHandler> contentHandlerSupplier;
|
||||
private final Supplier<Metadata> metadataSupplier;
|
||||
private final Supplier<ParseContext> parseContextSupplier;
|
||||
//文件前缀
|
||||
private static final Set<String> FILE_SUFFIX = new HashSet<>(Arrays.asList("docx", "doc", "pptx", "ppt", "xlsx", "xls"));
|
||||
|
||||
public TikaDocumentParser() {
|
||||
this((Supplier) ((Supplier) null), (Supplier) null, (Supplier) null, (Supplier) null);
|
||||
@ -77,16 +71,22 @@ public class TikaDocumentParser {
|
||||
InputStream isForParsing = Files.newInputStream(file.toPath());
|
||||
// 使用 Tika 自动检测 MIME 类型
|
||||
String fileName = file.getName().toLowerCase();
|
||||
//后缀
|
||||
String ext = FilenameUtils.getExtension(fileName);
|
||||
if (fileName.endsWith(".txt")
|
||||
|| fileName.endsWith(".md")
|
||||
|| fileName.endsWith(".pdf")) {
|
||||
return extractByTika(isForParsing);
|
||||
//update-begin---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
||||
} else if (FILE_SUFFIX.contains(ext.toLowerCase())) {
|
||||
return parseDocExcelPdfUsingApachePoi(file);
|
||||
//update-end---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
||||
} else if (fileName.endsWith(".docx")) {
|
||||
return extractTextFromDocx(isForParsing);
|
||||
} else if (fileName.endsWith(".doc")) {
|
||||
return extractTextFromDoc(isForParsing);
|
||||
} else if (fileName.endsWith(".xlsx")) {
|
||||
return extractTextFromExcel(isForParsing);
|
||||
} else if (fileName.endsWith(".xls")) {
|
||||
return extractTextFromExcel(isForParsing);
|
||||
} else if (fileName.endsWith(".pptx")) {
|
||||
return extractTextFromPptx(isForParsing);
|
||||
} else if (fileName.endsWith(".ppt")) {
|
||||
return extractTextFromPpt(isForParsing);
|
||||
} else {
|
||||
throw new IllegalArgumentException("不支持的文件格式: " + FilenameUtils.getExtension(fileName));
|
||||
}
|
||||
@ -95,27 +95,6 @@ public class TikaDocumentParser {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* langchain4j 内部解析器
|
||||
* @param file
|
||||
* @return
|
||||
*/
|
||||
public Document parseDocExcelPdfUsingApachePoi(File file) {
|
||||
AssertUtils.assertNotEmpty("请选择文件", file);
|
||||
try (InputStream inputStream = Files.newInputStream(file.toPath())) {
|
||||
ApachePoiDocumentParser parser = new ApachePoiDocumentParser();
|
||||
Document document = parser.parse(inputStream);
|
||||
if (document == null || Utils.isNullOrBlank(document.text())) {
|
||||
return null;
|
||||
}
|
||||
return document;
|
||||
} catch (BlankDocumentException e) {
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Document tryExtractDocOrDocx(InputStream inputStream) throws IOException {
|
||||
try {
|
||||
// 先尝试 DOCX(基于 OPC XML 格式)
|
||||
|
||||
@ -102,11 +102,4 @@ public class AiragKnowledge implements Serializable {
|
||||
@Excel(name = "状态", width = 15)
|
||||
@Schema(description = "状态")
|
||||
private java.lang.String status;
|
||||
|
||||
/**
|
||||
* 类型(knowledge知识 memory 记忆)
|
||||
*/
|
||||
@Excel(name="类型(knowledge知识 memory 记忆)", width = 15)
|
||||
@Schema(description = "类型(knowledge知识 memory 记忆)")
|
||||
private java.lang.String type;
|
||||
}
|
||||
|
||||
@ -12,16 +12,13 @@ import org.jeecg.ai.handler.LLMHandler;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.common.consts.AiragConsts;
|
||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
||||
import org.jeecg.modules.airag.common.handler.McpToolProviderWrapper;
|
||||
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||
import org.jeecg.modules.airag.llm.mapper.AiragMcpMapper;
|
||||
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
|
||||
import org.jeecg.config.AiRagConfigBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
@ -57,8 +54,6 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
@Autowired
|
||||
LLMHandler llmHandler;
|
||||
|
||||
@Autowired
|
||||
AiRagConfigBean aiRagConfigBean;
|
||||
|
||||
@Value(value = "${jeecg.path.upload:}")
|
||||
private String uploadpath;
|
||||
@ -290,7 +285,7 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
|
||||
// 默认超时时间
|
||||
if(oConvertUtils.isObjectEmpty(params.getTimeout())){
|
||||
params.setTimeout(AiragConsts.DEFAULT_TIMEOUT);
|
||||
params.setTimeout(60);
|
||||
}
|
||||
|
||||
//deepseek-reasoner 推理模型不支持插件tool
|
||||
@ -306,7 +301,6 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
/**
|
||||
* 构造插件和MCP工具
|
||||
* for [QQYUN-12453]【AI】支持插件
|
||||
* for [QQYUN-9234] MCP服务连接关闭 - 使用包装器保存连接引用
|
||||
* @param params
|
||||
* @author chenrui
|
||||
* @date 2025/10/31 14:04
|
||||
@ -316,7 +310,6 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
|
||||
if(oConvertUtils.isObjectNotEmpty(pluginIds)){
|
||||
List<McpToolProvider> mcpToolProviders = new ArrayList<>();
|
||||
List<McpToolProviderWrapper> mcpToolProviderWrappers = new ArrayList<>();
|
||||
Map<ToolSpecification, ToolExecutor> pluginTools = new HashMap<>();
|
||||
|
||||
for (String pluginId : pluginIds.stream().distinct().collect(Collectors.toList())) {
|
||||
@ -332,18 +325,15 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
}
|
||||
|
||||
if ("mcp".equalsIgnoreCase(category)) {
|
||||
// MCP类型:构建McpToolProviderWrapper(包含连接引用用于后续关闭)
|
||||
// for [QQYUN-9234] MCP服务连接关闭
|
||||
McpToolProviderWrapper wrapper = buildMcpToolProviderWrapper(
|
||||
// MCP类型:构建McpToolProvider
|
||||
McpToolProvider mcpToolProvider = buildMcpToolProvider(
|
||||
airagMcp.getName(),
|
||||
airagMcp.getType(),
|
||||
airagMcp.getEndpoint(),
|
||||
airagMcp.getHeaders(),
|
||||
aiRagConfigBean.getAllowSensitiveNodes()
|
||||
airagMcp.getHeaders()
|
||||
);
|
||||
if (wrapper != null) {
|
||||
mcpToolProviders.add(wrapper.getMcpToolProvider());
|
||||
mcpToolProviderWrappers.add(wrapper);
|
||||
if (mcpToolProvider != null) {
|
||||
mcpToolProviders.add(mcpToolProvider);
|
||||
}
|
||||
} else if ("plugin".equalsIgnoreCase(category)) {
|
||||
// 插件类型:构建ToolSpecification和ToolExecutor
|
||||
@ -358,12 +348,6 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
if (!mcpToolProviders.isEmpty()) {
|
||||
params.setMcpToolProviders(mcpToolProviders);
|
||||
}
|
||||
|
||||
// 保存MCP连接包装器,用于后续关闭
|
||||
// for [QQYUN-9234] MCP服务连接关闭
|
||||
if (!mcpToolProviderWrappers.isEmpty()) {
|
||||
params.setMcpToolProviderWrappers(mcpToolProviderWrappers);
|
||||
}
|
||||
|
||||
// 设置插件工具
|
||||
if (!pluginTools.isEmpty()) {
|
||||
@ -417,129 +401,5 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
return imageContents;
|
||||
}
|
||||
|
||||
//================================================= begin【QQYUN-12145】【AI】AI 绘画创作 ========================================
|
||||
/**
|
||||
* 文本生成图片
|
||||
* @param modelId
|
||||
* @param messages
|
||||
* @param params
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public List<Map<String, Object>> imageGenerate(String modelId, String messages, AIChatParams params) {
|
||||
AssertUtils.assertNotEmpty("至少发送一条消息", messages);
|
||||
AssertUtils.assertNotEmpty("请选择图片大模型", modelId);
|
||||
AiragModel airagModel = airagModelMapper.getByIdIgnoreTenant(modelId);
|
||||
return this.imageGenerate(airagModel, messages, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本生成图片
|
||||
*
|
||||
* @param airagModel
|
||||
* @param messages
|
||||
* @param params
|
||||
* @return
|
||||
*/
|
||||
public List<Map<String, Object>> imageGenerate(AiragModel airagModel, String messages, AIChatParams params) {
|
||||
params = mergeParams(airagModel, params);
|
||||
try {
|
||||
return llmHandler.imageGenerate(messages, params);
|
||||
} catch (Exception e) {
|
||||
String errMsg = "调用绘画AI接口失败,详情请查看后台日志。";
|
||||
if (oConvertUtils.isNotEmpty(e.getMessage())) {
|
||||
// 根据常见异常关键字做细致翻译
|
||||
for (Map.Entry<String, String> entry : MODEL_ERROR_MAP.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
if (errMsg.contains(key)) {
|
||||
errMsg = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
log.error("AI模型调用异常: {}", errMsg, e);
|
||||
throw new JeecgBootException(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 图生图
|
||||
*
|
||||
* @param modelId
|
||||
* @param messages
|
||||
* @param images
|
||||
* @param params
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public List<Map<String, Object>> imageEdit(String modelId, String messages, List<String> images, AIChatParams params) {
|
||||
AiragModel airagModel = airagModelMapper.getByIdIgnoreTenant(modelId);
|
||||
params = mergeParams(airagModel, params);
|
||||
List<String> originalImageBase64List = getFirstImageBase64(images);
|
||||
try {
|
||||
return llmHandler.imageEdit(messages, originalImageBase64List, params);
|
||||
} catch (Exception e) {
|
||||
String errMsg = "调用绘画AI接口失败,详情请查看后台日志。";
|
||||
if (oConvertUtils.isNotEmpty(e.getMessage())) {
|
||||
// 根据常见异常关键字做细致翻译
|
||||
for (Map.Entry<String, String> entry : MODEL_ERROR_MAP.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
if (errMsg.contains(key)) {
|
||||
errMsg = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
log.error("AI模型调用异常: {}", errMsg, e);
|
||||
throw new JeecgBootException(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 需要将图片转换成Base64编码
|
||||
* @param images 图片路径列表
|
||||
* @return Base64编码字符串
|
||||
*/
|
||||
private List<String> getFirstImageBase64(List<String> images) {
|
||||
List<String> originalImageBase64List = new ArrayList<>();
|
||||
if (images != null && !images.isEmpty()) {
|
||||
for (String imageUrl : images) {
|
||||
Matcher matcher = LLMConsts.WEB_PATTERN.matcher(imageUrl);
|
||||
try {
|
||||
byte[] fileContent;
|
||||
if (matcher.matches()) {
|
||||
// 来源于网络
|
||||
java.net.URL url = new java.net.URL(imageUrl);
|
||||
java.net.URLConnection conn = url.openConnection();
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(10000);
|
||||
try (java.io.InputStream in = conn.getInputStream()) {
|
||||
java.io.ByteArrayOutputStream buffer = new java.io.ByteArrayOutputStream();
|
||||
int nRead;
|
||||
byte[] data = new byte[1024];
|
||||
while ((nRead = in.read(data, 0, data.length)) != -1) {
|
||||
buffer.write(data, 0, nRead);
|
||||
}
|
||||
buffer.flush();
|
||||
fileContent = buffer.toByteArray();
|
||||
}
|
||||
} else {
|
||||
// 本地文件
|
||||
String filePath = uploadpath + File.separator + imageUrl;
|
||||
Path path = Paths.get(filePath);
|
||||
fileContent = Files.readAllBytes(path);
|
||||
}
|
||||
originalImageBase64List.add(Base64.getEncoder().encodeToString(fileContent));
|
||||
} catch (Exception e) {
|
||||
log.error("图片读取失败: " + imageUrl, e);
|
||||
throw new JeecgBootException("图片读取失败: " + imageUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
return originalImageBase64List;
|
||||
}
|
||||
//================================================= end 【QQYUN-12145】【AI】AI 绘画创作 ========================================
|
||||
}
|
||||
|
||||
@ -17,17 +17,13 @@ import dev.langchain4j.store.embedding.EmbeddingMatch;
|
||||
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
|
||||
import dev.langchain4j.store.embedding.EmbeddingStore;
|
||||
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
|
||||
import dev.langchain4j.store.embedding.filter.Filter;
|
||||
import dev.langchain4j.store.embedding.filter.logical.And;
|
||||
import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.tika.parser.AutoDetectParser;
|
||||
import org.jeecg.ai.factory.AiModelFactory;
|
||||
import org.jeecg.ai.factory.AiModelOptions;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.system.util.JwtUtil;
|
||||
import org.jeecg.common.util.*;
|
||||
import org.jeecg.modules.airag.common.handler.IEmbeddingHandler;
|
||||
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
|
||||
@ -98,21 +94,11 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
*/
|
||||
private static final int DEFAULT_OVERLAP_SIZE = 50;
|
||||
|
||||
/**
|
||||
* 最大输出长度
|
||||
*/
|
||||
private static final int DEFAULT_MAX_OUTPUT_CHARS = 4000;
|
||||
|
||||
/**
|
||||
* 向量存储元数据:knowledgeId
|
||||
*/
|
||||
public static final String EMBED_STORE_METADATA_KNOWLEDGEID = "knowledgeId";
|
||||
|
||||
/**
|
||||
* 向量存储元数据: 用户账号
|
||||
*/
|
||||
public static final String EMBED_STORE_METADATA_USER_NAME = "username";
|
||||
|
||||
/**
|
||||
* 向量存储元数据:docId
|
||||
*/
|
||||
@ -123,11 +109,6 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
*/
|
||||
public static final String EMBED_STORE_METADATA_DOCNAME = "docName";
|
||||
|
||||
/**
|
||||
* 向量存储元数据:创建时间
|
||||
*/
|
||||
public static final String EMBED_STORE_CREATE_TIME = "createTime";
|
||||
|
||||
/**
|
||||
* 向量存储缓存
|
||||
*/
|
||||
@ -194,26 +175,7 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
.build();
|
||||
Metadata metadata = Metadata.metadata(EMBED_STORE_METADATA_DOCID, doc.getId())
|
||||
.put(EMBED_STORE_METADATA_KNOWLEDGEID, doc.getKnowledgeId())
|
||||
.put(EMBED_STORE_METADATA_DOCNAME, FilenameUtils.getName(doc.getTitle()))
|
||||
//初始化记忆库的时候添加创建时间选项
|
||||
.put(EMBED_STORE_CREATE_TIME, String.valueOf(doc.getCreateTime() != null ? doc.getCreateTime().getTime() : System.currentTimeMillis()));
|
||||
//update-begin---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
|
||||
//添加用户名字到元数据里面,用于记忆库中数据隔离
|
||||
String username = doc.getCreateBy();
|
||||
if (oConvertUtils.isEmpty(username)) {
|
||||
try {
|
||||
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
|
||||
String token = TokenUtils.getTokenByRequest(request);
|
||||
username = JwtUtil.getUsername(token);
|
||||
} catch (Exception e) {
|
||||
// ignore:token获取不到默认为admin
|
||||
username = "admin";
|
||||
}
|
||||
}
|
||||
if (oConvertUtils.isNotEmpty(username)) {
|
||||
metadata.put(EMBED_STORE_METADATA_USER_NAME, username);
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
|
||||
.put(EMBED_STORE_METADATA_DOCNAME, FilenameUtils.getName(doc.getTitle()));
|
||||
Document from = Document.from(content, metadata);
|
||||
ingestor.ingest(from);
|
||||
return metadata.toMap();
|
||||
@ -246,47 +208,16 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
}
|
||||
}
|
||||
|
||||
//命中的文档内容
|
||||
StringBuilder data = new StringBuilder();
|
||||
//update-begin---author:wangshuai---date:2026-01-04---for:【QQYUN-14479】给ai的时候需要限制几个字---
|
||||
//是否为记忆库
|
||||
boolean memoryMode = false;
|
||||
//记忆库只有一个
|
||||
if (knowIds.size() == 1) {
|
||||
String firstId = knowIds.get(0);
|
||||
if (oConvertUtils.isNotEmpty(firstId)) {
|
||||
AiragKnowledge k = airagKnowledgeMapper.getByIdIgnoreTenant(firstId);
|
||||
memoryMode = (k != null && LLMConsts.KNOWLEDGE_TYPE_MEMORY.equalsIgnoreCase(k.getType()));
|
||||
}
|
||||
}
|
||||
//如果是记忆库按照创建时间排序,如果不是按照score分值进行排序
|
||||
List<Map<String, Object>> prepared = documents.stream()
|
||||
.sorted(memoryMode
|
||||
? Comparator.comparingLong((Map<String, Object> doc) -> oConvertUtils.getLong(doc.get(EMBED_STORE_CREATE_TIME), 0L)).reversed()
|
||||
: Comparator.comparingDouble((Map<String, Object> doc) -> (Double) doc.get("score")).reversed())
|
||||
// 对documents按score降序排序并取前topNumber个
|
||||
List<Map<String, Object>> sortedDocuments = documents.stream()
|
||||
.sorted(Comparator.comparingDouble((Map<String, Object> doc) -> (Double) doc.get("score")).reversed())
|
||||
.limit(topNumber)
|
||||
.peek(doc -> data.append(doc.get("content")).append("\n"))
|
||||
.collect(Collectors.toList());
|
||||
List<Map<String, Object>> limited = new ArrayList<>();
|
||||
//将返回的结果按照最大的token进行长度限制
|
||||
for (Map<String, Object> doc : prepared) {
|
||||
if (limited.size() >= topNumber) {
|
||||
break;
|
||||
}
|
||||
String content = oConvertUtils.getString(doc.get("content"), "");
|
||||
int remain = DEFAULT_MAX_OUTPUT_CHARS - data.length();
|
||||
if (remain <= 0) {
|
||||
break;
|
||||
}
|
||||
//数据库中文本的长度和已经拼接的长度
|
||||
if (content.length() <= remain) {
|
||||
data.append(content).append("\n");
|
||||
limited.add(doc);
|
||||
} else {
|
||||
data.append(content, 0, remain);
|
||||
limited.add(doc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new KnowledgeSearchResult(data.toString(), limited);
|
||||
//update-end---author:wangshuai---date:2026-01-04---for:【QQYUN-14479】给ai的时候需要限制几个字---
|
||||
|
||||
return new KnowledgeSearchResult(data.toString(), sortedDocuments);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -313,31 +244,11 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
|
||||
topNumber = oConvertUtils.getInteger(topNumber, modelOp.getTopNumber());
|
||||
similarity = oConvertUtils.getDou(similarity, modelOp.getSimilarity());
|
||||
|
||||
//update-begin---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
|
||||
Filter filter = metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId);
|
||||
|
||||
// 记忆库的时候需要根据用户隔离
|
||||
if (LLMConsts.KNOWLEDGE_TYPE_MEMORY.equalsIgnoreCase(knowledge.getType())) {
|
||||
try {
|
||||
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
|
||||
String token = TokenUtils.getTokenByRequest(request);
|
||||
String username = JwtUtil.getUsername(token);
|
||||
if (oConvertUtils.isNotEmpty(username)) {
|
||||
filter = new And(filter, metadataKey(EMBED_STORE_METADATA_USER_NAME).isEqualTo(username));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
log.info("构建过滤器异常,{}",e.getMessage());
|
||||
}
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
|
||||
|
||||
EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
|
||||
.queryEmbedding(queryEmbedding)
|
||||
.maxResults(topNumber)
|
||||
.minScore(similarity)
|
||||
.filter(filter)
|
||||
.filter(metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId))
|
||||
.build();
|
||||
|
||||
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
|
||||
@ -351,9 +262,6 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
Metadata metadata = matchRes.embedded().metadata();
|
||||
data.put("chunk", metadata.getInteger("index"));
|
||||
data.put(EMBED_STORE_METADATA_DOCNAME, metadata.getString(EMBED_STORE_METADATA_DOCNAME));
|
||||
//查询返回的时候增加创建时间,用于排序
|
||||
String ct = metadata.getString(EMBED_STORE_CREATE_TIME);
|
||||
data.put(EMBED_STORE_CREATE_TIME, ct);
|
||||
return data;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
@ -387,32 +295,13 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
|
||||
topNumber = oConvertUtils.getInteger(topNumber, 5);
|
||||
similarity = oConvertUtils.getDou(similarity, 0.75);
|
||||
|
||||
//update-begin---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
|
||||
Filter filter = metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId);
|
||||
// 记忆库的时候需要根据用户隔离
|
||||
if (LLMConsts.KNOWLEDGE_TYPE_MEMORY.equalsIgnoreCase(knowledge.getType())) {
|
||||
try {
|
||||
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
|
||||
String token = TokenUtils.getTokenByRequest(request);
|
||||
String username = JwtUtil.getUsername(token);
|
||||
if (oConvertUtils.isNotEmpty(username)) {
|
||||
filter = new And(filter, metadataKey(EMBED_STORE_METADATA_USER_NAME).isEqualTo(username));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
log.info("构建过滤器异常,{}",e.getMessage());
|
||||
}
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
|
||||
|
||||
// 构建一个嵌入存储内容检索器,用于从嵌入存储中检索内容
|
||||
EmbeddingStoreContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
|
||||
.embeddingStore(embeddingStore)
|
||||
.embeddingModel(embeddingModel)
|
||||
.maxResults(topNumber)
|
||||
.minScore(similarity)
|
||||
.filter(filter)
|
||||
.filter(metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId))
|
||||
.build();
|
||||
retrievers.add(contentRetriever);
|
||||
}
|
||||
|
||||
@ -12,8 +12,6 @@ import org.jeecg.common.util.CommonUtils;
|
||||
import org.jeecg.common.util.RestUtil;
|
||||
import org.jeecg.common.util.TokenUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.common.consts.AiragConsts;
|
||||
import org.jeecg.modules.airag.flow.component.ToolsNode;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
@ -53,9 +51,8 @@ public class PluginToolBuilder {
|
||||
}
|
||||
|
||||
String baseUrl = airagMcp.getEndpoint();
|
||||
boolean isEmptyBaseUrl = oConvertUtils.isEmpty(baseUrl);
|
||||
// 如果baseUrl为空,使用当前系统地址
|
||||
if (isEmptyBaseUrl) {
|
||||
if (oConvertUtils.isEmpty(baseUrl)) {
|
||||
if (currentHttpRequest != null) {
|
||||
baseUrl = CommonUtils.getBaseUrl(currentHttpRequest);
|
||||
log.info("插件[{}]的BaseURL为空,使用系统地址: {}", airagMcp.getName(), baseUrl);
|
||||
@ -67,10 +64,7 @@ public class PluginToolBuilder {
|
||||
|
||||
// 解析headers
|
||||
Map<String, String> headersMap = parseHeaders(airagMcp.getHeaders());
|
||||
|
||||
// 判断是否需要加签
|
||||
boolean isNeedSign = isEmptyBaseUrl && ToolsNode.Helper.checkNeedSign(headersMap);
|
||||
|
||||
|
||||
// 解析并应用授权配置(从metadata中读取)
|
||||
applyAuthConfig(headersMap, airagMcp.getMetadata(), currentHttpRequest);
|
||||
|
||||
@ -82,7 +76,7 @@ public class PluginToolBuilder {
|
||||
|
||||
try {
|
||||
ToolSpecification spec = buildToolSpecification(toolConfig);
|
||||
ToolExecutor executor = buildToolExecutor(toolConfig, baseUrl, headersMap, isNeedSign);
|
||||
ToolExecutor executor = buildToolExecutor(toolConfig, baseUrl, headersMap);
|
||||
if (spec != null && executor != null) {
|
||||
tools.put(spec, executor);
|
||||
}
|
||||
@ -193,7 +187,7 @@ public class PluginToolBuilder {
|
||||
/**
|
||||
* 构建ToolExecutor
|
||||
*/
|
||||
private static ToolExecutor buildToolExecutor(JSONObject toolConfig, String baseUrl, Map<String, String> defaultHeaders, boolean isNeedSign) {
|
||||
private static ToolExecutor buildToolExecutor(JSONObject toolConfig, String baseUrl, Map<String, String> defaultHeaders) {
|
||||
String path = toolConfig.getString("path");
|
||||
String method = toolConfig.getString("method");
|
||||
JSONArray parameters = toolConfig.getJSONArray("parameters");
|
||||
@ -221,13 +215,8 @@ public class PluginToolBuilder {
|
||||
JSONObject urlVariables = buildUrlVariables(parameters, args);
|
||||
Object body = buildRequestBody(parameters, args, httpHeaders);
|
||||
|
||||
if (isNeedSign) {
|
||||
// 发送请求前加签
|
||||
ToolsNode.Helper.applySignature(url, httpHeaders, urlVariables, body);
|
||||
}
|
||||
|
||||
// 发送HTTP请求,增加超时时间
|
||||
ResponseEntity<String> response = RestUtil.request(url, httpMethod, httpHeaders, urlVariables, body, String.class, AiragConsts.DEFAULT_TIMEOUT * 1000);
|
||||
// 发送HTTP请求
|
||||
ResponseEntity<String> response = RestUtil.request(url, httpMethod, httpHeaders, urlVariables, body, String.class);
|
||||
|
||||
// 直接返回原始响应字符串,不进行解析
|
||||
return response.getBody() != null ? response.getBody() : "";
|
||||
@ -346,13 +335,7 @@ public class PluginToolBuilder {
|
||||
if (isQueryParam || !isOtherType) {
|
||||
Object value = args.get(paramName);
|
||||
if (value != null) {
|
||||
//如果是知识库的id赋值默认值
|
||||
if ("knowledgeId".equalsIgnoreCase(paramName)) {
|
||||
String defaultValue = param.getString("defaultValue");
|
||||
urlVariables.put(paramName, defaultValue);
|
||||
} else {
|
||||
urlVariables.put(paramName, value);
|
||||
}
|
||||
urlVariables.put(paramName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -409,13 +392,7 @@ public class PluginToolBuilder {
|
||||
|
||||
Object value = args.get(paramName);
|
||||
if (value != null) {
|
||||
//如果是知识库的id赋值默认值
|
||||
if ("knowledgeId".equalsIgnoreCase(paramName)) {
|
||||
String defaultValue = param.getString("defaultValue");
|
||||
body.put(paramName, defaultValue);
|
||||
} else {
|
||||
body.put(paramName, value);
|
||||
}
|
||||
body.put(paramName, value);
|
||||
} else {
|
||||
// 检查是否有默认值
|
||||
String defaultValue = param.getString("defaultValue");
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
package org.jeecg.modules.airag.llm.service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: 获取流程mcp服务
|
||||
* @Author: wangshuai
|
||||
* @Date: 2025-12-22 15:34:20
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IAiragFlowPluginService {
|
||||
|
||||
/**
|
||||
* 同步所有启用的流程到MCP插件配置
|
||||
*
|
||||
* @param flowIds 多个流程id
|
||||
*/
|
||||
Map<String, Object> getFlowsToPlugin(String flowIds);
|
||||
}
|
||||
@ -3,8 +3,6 @@ package org.jeecg.modules.airag.llm.service;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AIRag知识库
|
||||
*
|
||||
@ -13,12 +11,4 @@ import java.util.Map;
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IAiragKnowledgeService extends IService<AiragKnowledge> {
|
||||
|
||||
/**
|
||||
* 构建知识库的工具
|
||||
*
|
||||
* @param memoryId
|
||||
* @return Map<String, Object>
|
||||
*/
|
||||
Map<String, Object> getPluginMemory(String memoryId);
|
||||
}
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
package org.jeecg.modules.airag.llm.service.impl;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.airag.api.IAiragBaseApi;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.exception.JeecgBootBizTipException;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledgeDoc;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeDocService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* airag baseAPI 实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Primary
|
||||
@Service("airagBaseApiImpl")
|
||||
public class AiragBaseApiImpl implements IAiragBaseApi {
|
||||
|
||||
@Autowired
|
||||
private IAiragKnowledgeDocService airagKnowledgeDocService;
|
||||
|
||||
@Override
|
||||
public String knowledgeWriteTextDocument(String knowledgeId, String title, String content) {
|
||||
AssertUtils.assertNotEmpty("知识库ID不能为空", knowledgeId);
|
||||
AssertUtils.assertNotEmpty("写入内容不能为空", content);
|
||||
AiragKnowledgeDoc knowledgeDoc = new AiragKnowledgeDoc();
|
||||
knowledgeDoc.setKnowledgeId(knowledgeId);
|
||||
knowledgeDoc.setTitle(title);
|
||||
knowledgeDoc.setType(LLMConsts.KNOWLEDGE_DOC_TYPE_TEXT);
|
||||
knowledgeDoc.setContent(content);
|
||||
Result<?> result = airagKnowledgeDocService.editDocument(knowledgeDoc);
|
||||
if (!result.isSuccess()) {
|
||||
throw new JeecgBootBizTipException(result.getMessage());
|
||||
}
|
||||
if (knowledgeDoc.getId() == null) {
|
||||
throw new JeecgBootBizTipException("知识库文档ID为空");
|
||||
}
|
||||
log.info("[AI-KNOWLEDGE] 文档写入完成,知识库:{}, 文档ID:{}", knowledgeId, knowledgeDoc.getId());
|
||||
return knowledgeDoc.getId();
|
||||
}
|
||||
}
|
||||
@ -1,231 +0,0 @@
|
||||
package org.jeecg.modules.airag.llm.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
import dev.langchain4j.service.tool.ToolExecutor;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.constant.SymbolConstant;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.common.util.TokenUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.flow.consts.FlowConsts;
|
||||
import org.jeecg.modules.airag.flow.entity.AiragFlow;
|
||||
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
|
||||
import org.jeecg.modules.airag.flow.vo.api.SubFlowResult;
|
||||
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
|
||||
import org.jeecg.modules.airag.llm.consts.FlowPluginContent;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
||||
import org.jeecg.modules.airag.llm.handler.PluginToolBuilder;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragFlowPluginService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @Description: 流程同步到MCP服务实现类
|
||||
* @Author: wangshuai
|
||||
* @Date: 2025-12-22
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AiragFlowPluginServiceImpl implements IAiragFlowPluginService {
|
||||
|
||||
@Autowired
|
||||
private IAiragFlowService airagFlowService;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getFlowsToPlugin(String flowIds) {
|
||||
log.info("开始构建流程插件");
|
||||
// 1. 查询所有启用的流程
|
||||
LambdaQueryWrapper<AiragFlow> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(AiragFlow::getStatus, FlowConsts.FLOW_STATUS_ENABLE);
|
||||
queryWrapper.in(AiragFlow::getId, Arrays.asList(flowIds.split(SymbolConstant.COMMA)));
|
||||
List<AiragFlow> flows = airagFlowService.list(queryWrapper);
|
||||
HttpServletRequest httpServletRequest = SpringContextUtils.getHttpServletRequest();
|
||||
if (flows.isEmpty()) {
|
||||
log.info("当前应用所选流程没有启用的流程");
|
||||
return null;
|
||||
}
|
||||
//返回数据
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
//插件
|
||||
//插件id
|
||||
AiragMcp tool = new AiragMcp();
|
||||
// 2. 构建插件
|
||||
String id = UUID.randomUUID().toString().replace("-", "");
|
||||
tool.setId(id);
|
||||
// 插件名称
|
||||
tool.setName(FlowPluginContent.PLUGIN_NAME);
|
||||
// 描述
|
||||
tool.setDescr(FlowPluginContent.PLUGIN_DESC);
|
||||
tool.setStatus(FlowConsts.FLOW_STATUS_ENABLE);
|
||||
tool.setSynced(CommonConstant.STATUS_1_INT);
|
||||
tool.setCategory("plugin");
|
||||
tool.setEndpoint("");
|
||||
int toolCount = 0;
|
||||
//构建拆件工具
|
||||
for (AiragFlow flow : flows) {
|
||||
try {
|
||||
|
||||
SubFlowResult subFlow = new SubFlowResult(flow);
|
||||
// 获取入参参数
|
||||
JSONArray parameter = getInputParameter(flow, subFlow);
|
||||
// 获取出参参数
|
||||
JSONArray outParams = getOutputParameter(flow, subFlow);
|
||||
// name必须符合 ^[a-zA-Z0-9_-]+$
|
||||
String validToolName = "flow_" + flow.getId();
|
||||
// 将原始名称拼接到描述中
|
||||
String description = flow.getName();
|
||||
if (oConvertUtils.isNotEmpty(flow.getDescr())) {
|
||||
description += " : " + flow.getDescr();
|
||||
}
|
||||
//构造工具参数
|
||||
String flowTool = buildParameter(parameter, outParams, flow.getId(), tool.getTools(), validToolName, description);
|
||||
tool.setTools(flowTool);
|
||||
toolCount++;
|
||||
} catch (Exception e) {
|
||||
log.error("处理流程[{}]转换插件失败: {}", flow.getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
String tenantId = TokenUtils.getTenantIdByRequest(httpServletRequest);
|
||||
//构建元数据(请求头)
|
||||
String meataData = buildMetadata(toolCount, tenantId);
|
||||
tool.setMetadata(meataData);
|
||||
Map<ToolSpecification, ToolExecutor> tools = PluginToolBuilder.buildTools(tool, httpServletRequest);
|
||||
result.put("pluginTool", tools);
|
||||
result.put("pluginId", id);
|
||||
log.info("构建流程插件结束");
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建元数据
|
||||
*
|
||||
* @param toolCount
|
||||
* @param tenantId
|
||||
*/
|
||||
private String buildMetadata(int toolCount, String tenantId) {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put(FlowPluginContent.TOKEN_PARAM_NAME, FlowPluginContent.X_ACCESS_TOKEN);
|
||||
jsonObject.put(FlowPluginContent.TOOL_COUNT, toolCount);
|
||||
jsonObject.put(FlowPluginContent.AUTH_TYPE, FlowPluginContent.TOKEN);
|
||||
jsonObject.put(FlowPluginContent.TOKEN_PARAM_VALUE, "");
|
||||
jsonObject.put(CommonConstant.TENANT_ID, oConvertUtils.getInt(tenantId, 0));
|
||||
return jsonObject.toJSONString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建参数
|
||||
*
|
||||
* @param parameter
|
||||
* @param outParams
|
||||
* @param flowId
|
||||
* @param tools
|
||||
* @param description
|
||||
* @param name
|
||||
*/
|
||||
private String buildParameter(JSONArray parameter, JSONArray outParams, String flowId, String tools, String name, String description) {
|
||||
JSONArray paramArray = new JSONArray();
|
||||
JSONObject parameterObject = new JSONObject();
|
||||
parameterObject.put(FlowPluginContent.NAME, name);
|
||||
parameterObject.put(FlowPluginContent.DESCRIPTION, description);
|
||||
parameterObject.put(FlowPluginContent.PATH, FlowPluginContent.PLUGIN_REQUEST_URL + flowId);
|
||||
parameterObject.put(FlowPluginContent.METHOD, FlowPluginContent.POST);
|
||||
parameterObject.put(FlowPluginContent.ENABLED, true);
|
||||
parameterObject.put(FlowPluginContent.PARAMETERS, parameter);
|
||||
parameterObject.put(FlowPluginContent.RESPONSES, outParams);
|
||||
if (oConvertUtils.isNotEmpty(tools)) {
|
||||
paramArray = JSONArray.parseArray(tools);
|
||||
paramArray.add(parameterObject);
|
||||
} else {
|
||||
paramArray.add(parameterObject);
|
||||
}
|
||||
return paramArray.toJSONString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数
|
||||
*
|
||||
* @param flow
|
||||
* @param subFlow
|
||||
*/
|
||||
private JSONArray getInputParameter(AiragFlow flow, SubFlowResult subFlow) {
|
||||
JSONArray parameters = new JSONArray();
|
||||
String metadata = flow.getMetadata();
|
||||
if (oConvertUtils.isNotEmpty(metadata)) {
|
||||
JSONObject jsonObject = JSONObject.parseObject(metadata);
|
||||
if (jsonObject.containsKey(FlowPluginContent.INPUTS)) {
|
||||
JSONArray jsonArray = jsonObject.getJSONArray(FlowPluginContent.INPUTS);
|
||||
jsonArray.forEach(item -> {
|
||||
if (oConvertUtils.isNotEmpty(item.toString())) {
|
||||
JSONObject json = JSONObject.parseObject(item.toString());
|
||||
json.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||
}
|
||||
});
|
||||
parameters.addAll(jsonArray);
|
||||
}
|
||||
}
|
||||
//需要获取子流程的参数,子流程的参数是单独封装的,否则在流程执行的时候会报错缺少参数
|
||||
List<FlowNodeConfig.NodeParam> inputParams = subFlow.getInputParams();
|
||||
if (inputParams != null) {
|
||||
for (FlowNodeConfig.NodeParam param : inputParams) {
|
||||
JSONObject p = new JSONObject();
|
||||
// 参数名
|
||||
p.put(FlowPluginContent.NAME, param.getField());
|
||||
String paramDesc = param.getName();
|
||||
if (oConvertUtils.isEmpty(paramDesc)) {
|
||||
paramDesc = param.getField();
|
||||
}
|
||||
// 参数描述
|
||||
p.put(FlowPluginContent.DESCRIPTION, paramDesc);
|
||||
// 类型
|
||||
p.put(FlowPluginContent.TYPE, oConvertUtils.getString(param.getType(), FlowPluginContent.TYPE_STRING));
|
||||
// 所有参数都在Body中
|
||||
p.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||
boolean required = param.getRequired() != null && param.getRequired();
|
||||
p.put(FlowPluginContent.REQUIRED, required);
|
||||
parameters.add(p);
|
||||
}
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建返回值
|
||||
*/
|
||||
private JSONArray getOutputParameter(AiragFlow flow, SubFlowResult subFlow) {
|
||||
JSONArray parameters = new JSONArray();
|
||||
String metadata = flow.getMetadata();
|
||||
if (oConvertUtils.isNotEmpty(metadata)) {
|
||||
JSONObject jsonObject = JSONObject.parseObject(metadata);
|
||||
if (jsonObject.containsKey(FlowPluginContent.OUTPUTS)) {
|
||||
JSONArray jsonArray = jsonObject.getJSONArray(FlowPluginContent.OUTPUTS);
|
||||
parameters.addAll(jsonArray);
|
||||
}
|
||||
}
|
||||
// List<FlowNodeConfig.NodeParam> outputParams = subFlow.getOutputParams();
|
||||
// if (outputParams != null) {
|
||||
// for (FlowNodeConfig.NodeParam param : outputParams) {
|
||||
// JSONObject p = new JSONObject();
|
||||
// // 参数名
|
||||
// p.put("name", param.getField());
|
||||
// String paramDesc = param.getName();
|
||||
// if (oConvertUtils.isEmpty(paramDesc)) {
|
||||
// paramDesc = param.getField();
|
||||
// }
|
||||
// // 参数描述
|
||||
// p.put("description", paramDesc);
|
||||
// // 类型
|
||||
// p.put("type", oConvertUtils.getString(param.getType(), "String"));
|
||||
// parameters.add(p);
|
||||
// }
|
||||
// }
|
||||
return parameters;
|
||||
}
|
||||
}
|
||||
@ -131,6 +131,7 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
|
||||
List<AiragKnowledgeDoc> docList = airagKnowledgeDocMapper.selectBatchIds(docIdList);
|
||||
AssertUtils.assertNotEmpty("文档不存在", docList);
|
||||
|
||||
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
|
||||
// 检查状态
|
||||
List<AiragKnowledgeDoc> knowledgeDocs = docList.stream()
|
||||
.filter(doc -> {
|
||||
@ -329,7 +330,6 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
|
||||
if (File.separator.equals("\\")) {
|
||||
// Windows path handling
|
||||
String escapedPath = uploadpath.replace("//", "\\\\");
|
||||
escapedPath = escapedPath.replace("/", "\\\\");
|
||||
relativePath = uploadedFile.getPath().replaceFirst("^" + escapedPath, "");
|
||||
} else {
|
||||
// Unix path handling
|
||||
|
||||
@ -1,215 +1,18 @@
|
||||
package org.jeecg.modules.airag.llm.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
import dev.langchain4j.service.tool.ToolExecutor;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.util.DateUtils;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.common.util.TokenUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.flow.consts.FlowConsts;
|
||||
import org.jeecg.modules.airag.llm.consts.FlowPluginContent;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
||||
import org.jeecg.modules.airag.llm.handler.PluginToolBuilder;
|
||||
import org.jeecg.modules.airag.llm.mapper.AiragKnowledgeMapper;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: AIRag知识库
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-18
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AiragKnowledgeServiceImpl extends ServiceImpl<AiragKnowledgeMapper, AiragKnowledge> implements IAiragKnowledgeService {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getPluginMemory(String memoryId) {
|
||||
//step 1获取知识库
|
||||
AiragKnowledge airagKnowledge = this.baseMapper.selectById(memoryId);
|
||||
if(airagKnowledge == null){
|
||||
return null;
|
||||
}
|
||||
return this.getKnowledgeToPlugin(memoryId,airagKnowledge.getDescr());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件信息
|
||||
*
|
||||
* @param knowledgeId
|
||||
* @param descr
|
||||
* @return
|
||||
*/
|
||||
public Map<String, Object> getKnowledgeToPlugin(String knowledgeId, String descr) {
|
||||
//step1 构建插件
|
||||
log.info("开始构建记忆库插件");
|
||||
if (oConvertUtils.isEmpty(knowledgeId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
HttpServletRequest httpServletRequest = SpringContextUtils.getHttpServletRequest();
|
||||
|
||||
//返回数据
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
//插件
|
||||
//插件id
|
||||
AiragMcp tool = new AiragMcp();
|
||||
// 2. 构建插件
|
||||
tool.setId(knowledgeId);
|
||||
// 插件名称
|
||||
tool.setName(FlowPluginContent.PLUGIN_MEMORY_NAME);
|
||||
// 描述
|
||||
tool.setDescr(FlowPluginContent.PLUGIN_MEMORY_DESC);
|
||||
tool.setStatus(FlowConsts.FLOW_STATUS_ENABLE);
|
||||
tool.setSynced(CommonConstant.STATUS_1_INT);
|
||||
tool.setCategory("plugin");
|
||||
tool.setEndpoint("");
|
||||
|
||||
JSONArray toolsArray = new JSONArray();
|
||||
// 添加记忆
|
||||
toolsArray.add(buildAddMemoryTool(knowledgeId,descr));
|
||||
// 查询记忆
|
||||
toolsArray.add(buildQueryMemoryTool(knowledgeId,descr));
|
||||
tool.setTools(toolsArray.toJSONString());
|
||||
String tenantId = TokenUtils.getTenantIdByRequest(httpServletRequest);
|
||||
//构建元数据(请求头)
|
||||
String meataData = buildMetadata(tenantId);
|
||||
tool.setMetadata(meataData);
|
||||
Map<ToolSpecification, ToolExecutor> tools = PluginToolBuilder.buildTools(tool, httpServletRequest);
|
||||
result.put("pluginTool", tools);
|
||||
result.put("pluginId", knowledgeId);
|
||||
log.info("构建记忆库插件结束");
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建元数据
|
||||
*
|
||||
* @param tenantId
|
||||
*/
|
||||
private String buildMetadata(String tenantId) {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put(FlowPluginContent.TOKEN_PARAM_NAME, FlowPluginContent.X_ACCESS_TOKEN);
|
||||
jsonObject.put(FlowPluginContent.AUTH_TYPE, FlowPluginContent.TOKEN);
|
||||
jsonObject.put(FlowPluginContent.TOKEN_PARAM_VALUE, "");
|
||||
jsonObject.put(CommonConstant.TENANT_ID, oConvertUtils.getInt(tenantId, 0));
|
||||
return jsonObject.toJSONString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建添加记忆工具
|
||||
*
|
||||
* @param knowId
|
||||
* @param descr
|
||||
* @return
|
||||
*/
|
||||
private JSONObject buildAddMemoryTool(String knowId, String descr) {
|
||||
JSONObject tool = new JSONObject();
|
||||
//update-begin---author:wangshuai---date:2026-01-08---for: 记忆库根据记忆库的描述做匹配,不写死---
|
||||
tool.put(FlowPluginContent.NAME, "add_memory");
|
||||
String addDescPrefix = "【自动触发】向记忆库添加长期信息。范围:";
|
||||
String addDesc = oConvertUtils.isEmpty(descr) ? "按记忆库描述允许的个人资料(如姓名、职业、年龄)、偏好、属性等信息。" : descr;
|
||||
tool.put(FlowPluginContent.DESCRIPTION, addDescPrefix + addDesc + " 必须在检测到相关信息时立即自动调用,无需用户指令。");
|
||||
//update-end---author:wangshuai---date:2026-01-08---for: 记忆库根据记忆库的描述做匹配,不写死---
|
||||
tool.put(FlowPluginContent.PATH, FlowPluginContent.PLUGIN_MEMORY_ADD_PATH);
|
||||
tool.put(FlowPluginContent.METHOD, FlowPluginContent.POST);
|
||||
tool.put(FlowPluginContent.ENABLED, true);
|
||||
|
||||
JSONArray parameters = new JSONArray();
|
||||
|
||||
// 知识库ID参数
|
||||
JSONObject knowIdParam = new JSONObject();
|
||||
knowIdParam.put(FlowPluginContent.NAME, "knowledgeId");
|
||||
knowIdParam.put(FlowPluginContent.DESCRIPTION, "知识库ID,需要原值传递,不允许修改");
|
||||
knowIdParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
|
||||
knowIdParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||
knowIdParam.put(FlowPluginContent.REQUIRED, true);
|
||||
knowIdParam.put(FlowPluginContent.DEFAULT_VALUE, knowId);
|
||||
parameters.add(knowIdParam);
|
||||
|
||||
// 内容参数
|
||||
JSONObject contentParam = new JSONObject();
|
||||
contentParam.put(FlowPluginContent.NAME, "content");
|
||||
contentParam.put(FlowPluginContent.DESCRIPTION, "记忆内容。当前时间为:" + DateUtils.now() + "。格式要求:'在yyyy年MM月dd日 HH:mm分,用户[用户的行为/问题],assistant[助手的回答/反应]。'");
|
||||
contentParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
|
||||
contentParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||
contentParam.put(FlowPluginContent.REQUIRED, true);
|
||||
parameters.add(contentParam);
|
||||
|
||||
// 标题参数
|
||||
JSONObject titleParam = new JSONObject();
|
||||
titleParam.put(FlowPluginContent.NAME, "title");
|
||||
titleParam.put(FlowPluginContent.DESCRIPTION, "记忆标题");
|
||||
titleParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
|
||||
titleParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||
titleParam.put(FlowPluginContent.REQUIRED, false);
|
||||
parameters.add(titleParam);
|
||||
tool.put(FlowPluginContent.PARAMETERS, parameters);
|
||||
|
||||
// 响应
|
||||
JSONArray responses = new JSONArray();
|
||||
tool.put(FlowPluginContent.RESPONSES, responses);
|
||||
|
||||
return tool;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询记忆工具
|
||||
*
|
||||
* @param knowId
|
||||
* @param descr
|
||||
* @return
|
||||
*/
|
||||
private JSONObject buildQueryMemoryTool(String knowId, String descr) {
|
||||
JSONObject tool = new JSONObject();
|
||||
//update-begin---author:wangshuai---date:2026-01-08---for: 记忆库根据记忆库的描述做匹配,不写死---
|
||||
String addDescPrefix = "【自动触发】向记忆库检索信息。范围:";
|
||||
String addDesc = oConvertUtils.isEmpty(descr) ? "按记忆库描述允许的个人资料(如姓名、职业、年龄)、偏好、属性等信息。" : descr;
|
||||
tool.put(FlowPluginContent.NAME, "query_memory");
|
||||
tool.put(FlowPluginContent.DESCRIPTION, addDescPrefix + addDesc + " 必须在检测到相关信息时立即自动调用,无需用户指令。");
|
||||
//update-end---author:wangshuai---date:2026-01-08---for: 记忆库根据记忆库的描述做匹配,不写死---
|
||||
tool.put(FlowPluginContent.PATH, FlowPluginContent.PLUGIN_MEMORY_QUERY_PATH);
|
||||
tool.put(FlowPluginContent.METHOD, FlowPluginContent.POST);
|
||||
tool.put(FlowPluginContent.ENABLED, true);
|
||||
|
||||
JSONArray parameters = new JSONArray();
|
||||
|
||||
// 知识库ID参数
|
||||
JSONObject knowIdParam = new JSONObject();
|
||||
knowIdParam.put(FlowPluginContent.NAME, "knowledgeId");
|
||||
knowIdParam.put(FlowPluginContent.DESCRIPTION, "知识库ID,需要原值传递,不允许修改");
|
||||
knowIdParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
|
||||
knowIdParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||
knowIdParam.put(FlowPluginContent.REQUIRED, true);
|
||||
knowIdParam.put(FlowPluginContent.DEFAULT_VALUE, knowId);
|
||||
parameters.add(knowIdParam);
|
||||
|
||||
// 查询内容参数
|
||||
JSONObject queryTextParam = new JSONObject();
|
||||
queryTextParam.put(FlowPluginContent.NAME, "queryText");
|
||||
queryTextParam.put(FlowPluginContent.DESCRIPTION, "查询内容");
|
||||
queryTextParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
|
||||
queryTextParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||
queryTextParam.put(FlowPluginContent.REQUIRED, true);
|
||||
parameters.add(queryTextParam);
|
||||
|
||||
tool.put(FlowPluginContent.PARAMETERS, parameters);
|
||||
// 响应
|
||||
JSONArray responses = new JSONArray();
|
||||
tool.put(FlowPluginContent.RESPONSES, responses);
|
||||
|
||||
return tool;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -10,14 +10,11 @@ import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
import dev.langchain4j.mcp.client.DefaultMcpClient;
|
||||
import dev.langchain4j.mcp.client.McpClient;
|
||||
import dev.langchain4j.mcp.client.transport.http.HttpMcpTransport;
|
||||
import dev.langchain4j.mcp.client.transport.http.StreamableHttpMcpTransport;
|
||||
import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
|
||||
import dev.langchain4j.model.chat.request.json.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.AiRagConfigBean;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
||||
import org.jeecg.modules.airag.llm.mapper.AiragMcpMapper;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragMcpService;
|
||||
@ -25,7 +22,6 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -35,15 +31,12 @@ import java.util.stream.Collectors;
|
||||
* @Date: 2025-10-20
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@SuppressWarnings("removal")
|
||||
@Service("airagMcpServiceImpl")
|
||||
@Slf4j
|
||||
public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> implements IAiragMcpService {
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper; // 使用全局配置的 Jackson ObjectMapper
|
||||
@Autowired
|
||||
private AiRagConfigBean aiRagConfigBean;
|
||||
|
||||
/**
|
||||
* 新增或编辑Mcpserver
|
||||
@ -132,7 +125,7 @@ public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> i
|
||||
Map<String, String> headers = null;
|
||||
if (oConvertUtils.isNotEmpty(mcp.getHeaders())) {
|
||||
try {
|
||||
headers = JSONObject.parseObject(mcp.getHeaders(), new com.alibaba.fastjson.TypeReference<Map<String, String>>() {});
|
||||
headers = JSONObject.parseObject(mcp.getHeaders(), Map.class);
|
||||
} catch (JSONException e) {
|
||||
headers = null;
|
||||
}
|
||||
@ -143,53 +136,19 @@ public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> i
|
||||
McpClient mcpClient = null;
|
||||
try {
|
||||
if ("sse".equalsIgnoreCase(type)) {
|
||||
//TODO 1.4.0-beta10被弃用,推荐使用http
|
||||
log.info("[MCP]使用SSE协议(HttpMcpTransport), endpoint:{}", endpoint);
|
||||
HttpMcpTransport.Builder builder = HttpMcpTransport.builder()
|
||||
HttpMcpTransport.Builder builder = new HttpMcpTransport.Builder()
|
||||
.sseUrl(endpoint)
|
||||
.logRequests(true)
|
||||
.logResponses(true);
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
builder.customHeaders(headers);
|
||||
}
|
||||
mcpClient = new DefaultMcpClient.Builder().transport(builder.build()).build();
|
||||
} else if ("stdio".equalsIgnoreCase(type)) {
|
||||
//update-begin---author:wangshuai---date:2025-12-18---for:【QQYUN-14242】【AI】添加参数控制 是否开启 默认禁用 stdio 调用执行命令---
|
||||
String openSafe = aiRagConfigBean.getAllowSensitiveNodes();
|
||||
if(oConvertUtils.isNotEmpty(openSafe) && openSafe.toLowerCase().contains("stdio")) {
|
||||
log.info("[MCP]使用STDIO协议(StdioMcpTransport), endpoint:{}", endpoint);
|
||||
// stdio 类型:endpoint 可能是一个命令行
|
||||
// Windows 下需要通过 cmd.exe /c 来执行命令,否则找不到 npx 等程序
|
||||
List<String> cmdParts;
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
if (os.contains("win")) {
|
||||
// Windows: 使用 cmd.exe /c 执行
|
||||
cmdParts = new ArrayList<>();
|
||||
cmdParts.add("cmd.exe");
|
||||
cmdParts.add("/c");
|
||||
cmdParts.add(endpoint.trim());
|
||||
} else {
|
||||
// Linux/Mac: 使用 sh -c 执行
|
||||
cmdParts = new ArrayList<>();
|
||||
cmdParts.add("sh");
|
||||
cmdParts.add("-c");
|
||||
cmdParts.add(endpoint.trim());
|
||||
}
|
||||
log.info("[MCP]执行stdio命令: {}", cmdParts);
|
||||
StdioMcpTransport.Builder builder = new StdioMcpTransport.Builder()
|
||||
.command(cmdParts)
|
||||
.environment(headers);
|
||||
mcpClient = new DefaultMcpClient.Builder().transport(builder.build()).build();
|
||||
} else {
|
||||
String disabledMsg = "stdio 功能已禁用。若需启用,请在 yml 的 jeecg.airag.allow-sensitive-nodes 中加入 stdio。";
|
||||
log.warn("[MCP]{}", disabledMsg);
|
||||
return Result.error(disabledMsg);
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-12-19---for:【QQYUN-14242】【AI】添加参数控制 是否开启 默认禁用 stdio 调用执行命令---
|
||||
}else if("http".equalsIgnoreCase(type)){
|
||||
log.info("[MCP]使用HTTP协议(StreamableHttpMcpTransport), endpoint:{}", endpoint);
|
||||
//增加http选项
|
||||
mcpClient = mcpHttpCreate(endpoint,headers);
|
||||
// stdio 类型:endpoint 可能是一个命令行,需要拆分为命令列表
|
||||
// List<String> cmdParts = Arrays.asList(endpoint.trim().split("\\s+"));
|
||||
// StdioMcpTransport.Builder builder = new StdioMcpTransport.Builder()
|
||||
// .command(cmdParts)
|
||||
// .environment(headers);
|
||||
// mcpClient = new DefaultMcpClient.Builder().transport(builder.build()).build();
|
||||
return Result.error("不支持的MCP类型:" + type);
|
||||
} else {
|
||||
return Result.error("不支持的MCP类型:" + type);
|
||||
}
|
||||
@ -245,29 +204,6 @@ public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> i
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* mcp插件http创建
|
||||
*
|
||||
* @param endpoint
|
||||
* @param headers
|
||||
* @return
|
||||
*/
|
||||
private McpClient mcpHttpCreate(String endpoint, Map<String, String> headers) {
|
||||
StreamableHttpMcpTransport.Builder builder = new StreamableHttpMcpTransport.Builder()
|
||||
.url(endpoint)
|
||||
.timeout(Duration.ofMinutes(60))
|
||||
.logRequests(true)
|
||||
.logResponses(true);
|
||||
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
builder.customHeaders(headers);
|
||||
}
|
||||
|
||||
return new DefaultMcpClient.Builder()
|
||||
.transport(builder.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
// 安全序列化单个对象为 JSON 字符串
|
||||
private String safeWriteJson(Object obj) {
|
||||
try {
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
package org.jeecg.modules.airag.prompts.consts;
|
||||
|
||||
/**
|
||||
* AI提示词常量类
|
||||
*
|
||||
*/
|
||||
public class AiPromptsConsts {
|
||||
|
||||
/**
|
||||
* 状态:进行中
|
||||
*/
|
||||
public static final String STATUS_RUNNING = "run";
|
||||
/**
|
||||
* 状态:完成
|
||||
*/
|
||||
public static final String STATUS_COMPLETED = "completed";
|
||||
/**
|
||||
* 状态:失败
|
||||
*/
|
||||
public static final String STATUS_FAILED = "failed";
|
||||
/**
|
||||
* 业务类型:评估器
|
||||
*/
|
||||
public static final String BIZ_TYPE_EVALUATOR = "evaluator";
|
||||
/**
|
||||
* 业务类型:轨迹
|
||||
*/
|
||||
public static final String BIZ_TYPE_TRACK = "track";
|
||||
|
||||
}
|
||||
@ -1,213 +0,0 @@
|
||||
package org.jeecg.modules.airag.prompts.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.prompts.consts.AiPromptsConsts;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||
import org.jeecg.modules.airag.prompts.service.IAiragExtDataService;
|
||||
import org.jeecg.modules.airag.prompts.vo.AiragDebugVo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: airag_ext_data
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-24
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Tag(name="airag_ext_data")
|
||||
@RestController
|
||||
@RequestMapping("/airag/extData")
|
||||
@Slf4j
|
||||
public class AiragExtDataController extends JeecgController<AiragExtData, IAiragExtDataService> {
|
||||
@Autowired
|
||||
private IAiragExtDataService airagExtDataService;
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
*
|
||||
* @param airagExtData
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "airag_ext_data-分页列表查询")
|
||||
@Operation(summary="airag_ext_data-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<AiragExtData>> queryPageList(AiragExtData airagExtData,
|
||||
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
|
||||
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<AiragExtData> queryWrapper = QueryGenerator.initQueryWrapper(airagExtData, req.getParameterMap());
|
||||
Page<AiragExtData> page = new Page<AiragExtData>(pageNo, pageSize);
|
||||
queryWrapper.eq("biz_type", AiPromptsConsts.BIZ_TYPE_EVALUATOR);
|
||||
IPage<AiragExtData> pageList = airagExtDataService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
/**
|
||||
* 调用轨迹列表查询
|
||||
*
|
||||
* @param airagExtData
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
@Operation(summary="airag_ext_data-分页列表查询")
|
||||
@GetMapping(value = "/getTrackList")
|
||||
public Result<IPage<AiragExtData>> getTrackList(AiragExtData airagExtData,
|
||||
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
|
||||
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<AiragExtData> queryWrapper = QueryGenerator.initQueryWrapper(airagExtData, req.getParameterMap());
|
||||
Page<AiragExtData> page = new Page<AiragExtData>(pageNo, pageSize);
|
||||
queryWrapper.eq("biz_type", AiPromptsConsts.BIZ_TYPE_TRACK);
|
||||
String metadata = airagExtData.getMetadata();
|
||||
if(oConvertUtils.isEmpty(metadata)){
|
||||
return Result.OK();
|
||||
}
|
||||
IPage<AiragExtData> pageList = airagExtDataService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*
|
||||
* @param airagExtData
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_ext_data-添加")
|
||||
@Operation(summary="airag_ext_data-添加")
|
||||
@PostMapping(value = "/add")
|
||||
public Result<String> add(@RequestBody AiragExtData airagExtData) {
|
||||
airagExtData.setBizType(AiPromptsConsts.BIZ_TYPE_EVALUATOR);
|
||||
airagExtDataService.save(airagExtData);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*
|
||||
* @param airagExtData
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_ext_data-编辑")
|
||||
@Operation(summary="airag_ext_data-编辑")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody AiragExtData airagExtData) {
|
||||
airagExtDataService.updateById(airagExtData);
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_ext_data-通过id删除")
|
||||
@Operation(summary="airag_ext_data-通过id删除")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name="id",required=true) String id) {
|
||||
airagExtDataService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*
|
||||
* @param ids
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_ext_data-批量删除")
|
||||
@Operation(summary="airag_ext_data-批量删除")
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
|
||||
this.airagExtDataService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "airag_ext_data-通过id查询")
|
||||
@Operation(summary="airag_ext_data-通过id查询")
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<AiragExtData> queryById(@RequestParam(name="id",required=true) String id) {
|
||||
AiragExtData airagExtData = airagExtDataService.getById(id);
|
||||
if(airagExtData==null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(airagExtData);
|
||||
}
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "airag_ext_data-通过id查询")
|
||||
@Operation(summary="airag_ext_data-通过id查询")
|
||||
@GetMapping(value = "/queryTrackById")
|
||||
public Result<List<AiragExtData>> queryTrackById(@RequestParam(name="id",required=true) String id) {
|
||||
AiragExtData airagExtData = airagExtDataService.getById(id);
|
||||
String status = airagExtData.getStatus();
|
||||
if(AiPromptsConsts.STATUS_RUNNING.equals(status)) {
|
||||
return Result.error("处理中,请稍后刷新");
|
||||
}
|
||||
List<AiragExtData> trackList = airagExtDataService.queryTrackById(id);
|
||||
return Result.OK(trackList);
|
||||
}
|
||||
/**
|
||||
* 构造器调试
|
||||
*
|
||||
* @param debugVo
|
||||
* @return
|
||||
*/
|
||||
@PostMapping(value = "/evaluator/debug")
|
||||
public Result<?> debugEvaluator(@RequestBody AiragDebugVo debugVo) {
|
||||
return airagExtDataService.debugEvaluator(debugVo);
|
||||
}
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param request
|
||||
* @param airagExtData
|
||||
*/
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, AiragExtData airagExtData) {
|
||||
return super.exportXls(request, airagExtData, AiragExtData.class, "airag_ext_data");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过excel导入数据
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
|
||||
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
|
||||
return super.importExcel(request, response, AiragExtData.class);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,167 +0,0 @@
|
||||
package org.jeecg.modules.airag.prompts.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
|
||||
import org.jeecg.modules.airag.prompts.service.IAiragPromptsService;
|
||||
import org.jeecg.modules.airag.prompts.vo.AiragExperimentVo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import java.util.Arrays;
|
||||
/**
|
||||
* @Description: airag_prompts
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-24
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Tag(name="airag_prompts")
|
||||
@RestController
|
||||
@RequestMapping("/airag/prompts")
|
||||
@Slf4j
|
||||
public class AiragPromptsController extends JeecgController<AiragPrompts, IAiragPromptsService> {
|
||||
@Autowired
|
||||
private IAiragPromptsService airagPromptsService;
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
*
|
||||
* @param airagPrompts
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "airag_prompts-分页列表查询")
|
||||
@Operation(summary="airag_prompts-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<AiragPrompts>> queryPageList(AiragPrompts airagPrompts,
|
||||
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
|
||||
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<AiragPrompts> queryWrapper = QueryGenerator.initQueryWrapper(airagPrompts, req.getParameterMap());
|
||||
Page<AiragPrompts> page = new Page<AiragPrompts>(pageNo, pageSize);
|
||||
IPage<AiragPrompts> pageList = airagPromptsService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*
|
||||
* @param airagPrompts
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_prompts-添加")
|
||||
@Operation(summary="airag_prompts-添加")
|
||||
@PostMapping(value = "/add")
|
||||
public Result<String> add(@RequestBody AiragPrompts airagPrompts) {
|
||||
airagPrompts.setDelFlag(CommonConstant.DEL_FLAG_0);
|
||||
airagPrompts.setStatus("0");
|
||||
airagPromptsService.save(airagPrompts);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*
|
||||
* @param airagPrompts
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_prompts-编辑")
|
||||
@Operation(summary="airag_prompts-编辑")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody AiragPrompts airagPrompts) {
|
||||
airagPromptsService.updateById(airagPrompts);
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_prompts-通过id删除")
|
||||
@Operation(summary="airag_prompts-通过id删除")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name="id",required=true) String id) {
|
||||
airagPromptsService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*
|
||||
* @param ids
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_prompts-批量删除")
|
||||
@Operation(summary="airag_prompts-批量删除")
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
|
||||
this.airagPromptsService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "airag_prompts-通过id查询")
|
||||
@Operation(summary="airag_prompts-通过id查询")
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<AiragPrompts> queryById(@RequestParam(name="id",required=true) String id) {
|
||||
AiragPrompts airagPrompts = airagPromptsService.getById(id);
|
||||
if(airagPrompts==null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(airagPrompts);
|
||||
}
|
||||
/**
|
||||
* 构造器调试
|
||||
*
|
||||
* @param experimentVo
|
||||
* @return
|
||||
*/
|
||||
@PostMapping(value = "/experiment")
|
||||
public Result<?> promptExperiment(@RequestBody AiragExperimentVo experimentVo, HttpServletRequest request) {
|
||||
return airagPromptsService.promptExperiment(experimentVo,request);
|
||||
}
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param request
|
||||
* @param airagPrompts
|
||||
*/
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, AiragPrompts airagPrompts) {
|
||||
return super.exportXls(request, airagPrompts, AiragPrompts.class, "airag_prompts");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过excel导入数据
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
|
||||
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
|
||||
return super.importExcel(request, response, AiragPrompts.class);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,99 +0,0 @@
|
||||
package org.jeecg.modules.airag.prompts.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Date;
|
||||
import java.math.BigDecimal;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import org.jeecg.common.constant.ProvinceCityArea;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import lombok.Data;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* @Description: airag_ext_data
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
@TableName("airag_ext_data")
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(description="airag_ext_data")
|
||||
public class AiragExtData implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**主键ID*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID")
|
||||
private java.lang.String id;
|
||||
/**业务类型标识(evaluator:评估器;track:测试追踪)*/
|
||||
@Excel(name = "业务类型标识(evaluator:评估器;track:测试追踪)", width = 15)
|
||||
@Schema(description = "业务类型标识(evaluator:评估器;track:测试追踪)")
|
||||
private java.lang.String bizType;
|
||||
/**名称*/
|
||||
@Excel(name = "名称", width = 15)
|
||||
@Schema(description = "名称")
|
||||
private java.lang.String name;
|
||||
/**描述信息*/
|
||||
@Excel(name = "描述信息", width = 15)
|
||||
@Schema(description = "描述信息")
|
||||
private java.lang.String descr;
|
||||
/**标签,多个用逗号分隔*/
|
||||
@Excel(name = "标签,多个用逗号分隔", width = 15)
|
||||
@Schema(description = "标签,多个用逗号分隔")
|
||||
private java.lang.String tags;
|
||||
/**实际存储内容,json*/
|
||||
@Excel(name = "实际存储内容,json", width = 15)
|
||||
@Schema(description = "实际存储内容,json")
|
||||
private java.lang.String dataValue;
|
||||
/**元数据,用于存储补充业务数据信息*/
|
||||
@Excel(name = "元数据,用于存储补充业务数据信息", width = 15)
|
||||
@Schema(description = "元数据,用于存储补充业务数据信息")
|
||||
private java.lang.String metadata;
|
||||
/**评测集数据*/
|
||||
@Excel(name = "评测集数据", width = 15)
|
||||
@Schema(description = "评测集数据")
|
||||
private java.lang.String datasetValue;
|
||||
/**创建人*/
|
||||
@Schema(description = "创建人")
|
||||
private java.lang.String createBy;
|
||||
/**创建时间*/
|
||||
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "创建时间")
|
||||
private java.util.Date createTime;
|
||||
/**修改人*/
|
||||
@Schema(description = "修改人")
|
||||
private java.lang.String updateBy;
|
||||
/**修改时间*/
|
||||
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "修改时间")
|
||||
private java.util.Date updateTime;
|
||||
/**所属部门*/
|
||||
@Schema(description = "所属部门")
|
||||
private java.lang.String sysOrgCode;
|
||||
/**租户id*/
|
||||
@Excel(name = "租户id", width = 15)
|
||||
@Schema(description = "租户id")
|
||||
private java.lang.String tenantId;
|
||||
/**状态*/
|
||||
@Excel(name = "状态(run:进行中 completed:已完成)", width = 15)
|
||||
@Schema(description = "状态(run:进行中 completed:已完成)")
|
||||
private java.lang.String status;
|
||||
/**版本*/
|
||||
@Excel(name = "版本", width = 15)
|
||||
@Schema(description = "版本")
|
||||
private java.lang.Integer version;
|
||||
}
|
||||
@ -1,108 +0,0 @@
|
||||
package org.jeecg.modules.airag.prompts.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Date;
|
||||
import java.math.BigDecimal;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import org.jeecg.common.constant.ProvinceCityArea;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import lombok.Data;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* @Description: airag_prompts
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
@TableName("airag_prompts")
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(description="airag_prompts")
|
||||
public class AiragPrompts implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**主键ID*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID")
|
||||
private java.lang.String id;
|
||||
/**提示词名称*/
|
||||
@Excel(name = "提示词名称", width = 15)
|
||||
@Schema(description = "提示词名称")
|
||||
private java.lang.String name;
|
||||
/**提示词名称*/
|
||||
@Excel(name = "提示key", width = 15)
|
||||
@Schema(description = "提示key")
|
||||
private java.lang.String promptKey;
|
||||
/**提示词功能描述*/
|
||||
@Excel(name = "提示词功能描述", width = 15)
|
||||
@Schema(description = "提示词功能描述")
|
||||
private java.lang.String description;
|
||||
/**提示词模板内容,支持变量占位符如 {{variable}}*/
|
||||
@Excel(name = "提示词模板内容,支持变量占位符如 {{variable}}", width = 15)
|
||||
@Schema(description = "提示词模板内容,支持变量占位符如 {{variable}}")
|
||||
private java.lang.String content;
|
||||
/**提示词分类*/
|
||||
@Excel(name = "提示词分类", width = 15)
|
||||
@Schema(description = "提示词分类")
|
||||
private java.lang.String category;
|
||||
/**标签,多个逗号分割*/
|
||||
@Excel(name = "标签,多个逗号分割", width = 15)
|
||||
@Schema(description = "标签,多个逗号分割")
|
||||
private java.lang.String tags;
|
||||
/**适配的大模型ID*/
|
||||
@Excel(name = "适配的大模型ID", width = 15)
|
||||
@Schema(description = "适配的大模型ID")
|
||||
private java.lang.String modelId;
|
||||
/**大模型的参数配置*/
|
||||
@Excel(name = "大模型的参数配置", width = 15)
|
||||
@Schema(description = "大模型的参数配置")
|
||||
private java.lang.String modelParam;
|
||||
/**状态(0:未发布 1:已发布)*/
|
||||
@Excel(name = "状态(0:未发布 1:已发布)", width = 15)
|
||||
@Schema(description = "状态(0:未发布 1:已发布)")
|
||||
private java.lang.String status;
|
||||
/**版本号(格式 0.0.1)*/
|
||||
@Excel(name = "版本号(格式 0.0.1)", width = 15)
|
||||
@Schema(description = "版本号(格式 0.0.1)")
|
||||
private java.lang.String version;
|
||||
/**删除状态(0未删除 1已删除)*/
|
||||
@Excel(name = "删除状态(0未删除 1已删除)", width = 15)
|
||||
@Schema(description = "删除状态(0未删除 1已删除)")
|
||||
@TableLogic
|
||||
private java.lang.Integer delFlag;
|
||||
/**创建人*/
|
||||
@Schema(description = "创建人")
|
||||
private java.lang.String createBy;
|
||||
/**创建日期*/
|
||||
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "创建日期")
|
||||
private java.util.Date createTime;
|
||||
/**更新人*/
|
||||
@Schema(description = "更新人")
|
||||
private java.lang.String updateBy;
|
||||
/**更新日期*/
|
||||
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "更新日期")
|
||||
private java.util.Date updateTime;
|
||||
/**所属部门*/
|
||||
@Schema(description = "所属部门")
|
||||
private java.lang.String sysOrgCode;
|
||||
/**租户id*/
|
||||
@Excel(name = "租户id", width = 15)
|
||||
@Schema(description = "租户id")
|
||||
private java.lang.String tenantId;
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
package org.jeecg.modules.airag.prompts.mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
/**
|
||||
* @Description: airag_ext_data
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface AiragExtDataMapper extends BaseMapper<AiragExtData> {
|
||||
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
package org.jeecg.modules.airag.prompts.mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
/**
|
||||
* @Description: airag_prompts
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface AiragPromptsMapper extends BaseMapper<AiragPrompts> {
|
||||
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.jeecg.modules.airag.prompts.mapper.AiragExtDataMapper">
|
||||
|
||||
</mapper>
|
||||
@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.jeecg.modules.airag.prompts.mapper.AiragPromptsMapper">
|
||||
|
||||
</mapper>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user