feat: init

This commit is contained in:
2025-03-31 23:54:04 +08:00
parent 0a2f08f583
commit 716571197f
30 changed files with 2031 additions and 0 deletions

2
.gitignore vendored
View File

@@ -10,6 +10,8 @@
# Mobile Tools for Java (J2ME)
.mtj.tmp/
.idea
target
# Package Files #
*.jar

126
pom.xml Normal file
View File

@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.metis</groupId>
<artifactId>metis</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.vserion>3.3.4</spring-boot.vserion>
<fastjson.version>2.0.45</fastjson.version>
<lombok.version>1.18.34</lombok.version>
<sanitizer.version>1.2.3</sanitizer.version>
<langchain4j.version>1.0.0-beta2</langchain4j.version>
<mybatis-plus.version>3.5.8</mybatis-plus.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.vserion}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
<version>1.0.0-beta2</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.mikesamuel</groupId>
<artifactId>json-sanitizer</artifactId>
<version>${sanitizer.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.22</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.6.2</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,15 @@
package com.metis;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class MetisApplication {
public static void main(String[] args) {
SpringApplication.run(MetisApplication.class, args);
}
}

View File

@@ -0,0 +1,18 @@
package com.metis.controller;
import com.metis.result.Result;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping
public Result<String> test() {
return Result.ok("测试成功");
}
}

View File

@@ -0,0 +1,45 @@
package com.metis.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* @author Clay
* @date 2022/10/30
*/
@Data
public class BaseEntity implements Serializable {
/**
* 创建者
*/
// private Object createBy;
/**
* 创建时间
*/
@JsonFormat(locale = "zh",timezone = "GMT+8",pattern = "yyyy-MM-dd")
private LocalDateTime createTime;
/**
* 更新者
*/
// private Object updateBy;
/**
* 更新时间
*/
@JsonFormat(locale = "zh",timezone = "GMT+8",pattern = "yyyy-MM-dd")
private LocalDateTime updateTime;
/**
* 逻辑删除字段
*/
private Boolean isDeleted;
}

View File

@@ -0,0 +1 @@
package com.metis.domain;

View File

@@ -0,0 +1,59 @@
package com.metis.enums;
import com.baomidou.mybatisplus.annotation.IEnum;
import java.util.Arrays;
public interface BaseEnum<T extends Enum<T>> extends IEnum<Integer> {
/**
* 获取编码
*
* @return {@link Integer}
*/
Integer getCode();
/**
* 获取名称
*
* @return {@link String}
*/
String getName();
/**
* 获取value值
*
* @return {@link Integer}
*/
@Override
default Integer getValue() {
return getCode();
}
/**
* 解析通过编码
*
* @param enumClass 枚举班
* @param code 编码
* @return {@link E}
*/
static <E extends Enum<E> & BaseEnum<E>> E parseByCode(Class<E> enumClass, Integer code) {
return Arrays.stream(enumClass.getEnumConstants())
.filter(e -> e.getCode().equals(code))
.findFirst()
.orElse(null);
}
/**
* 按名称解析
*
* @param enumClass 枚举班
* @param name 名称
* @return {@link E}
*/
static <E extends Enum<E> & BaseEnum<E>> E parseByName(Class<E> enumClass, String name) {
return Arrays.stream(enumClass.getEnumConstants())
.filter(e -> e.getName().equals(name))
.findFirst()
.orElse(null);
}
}

View File

@@ -0,0 +1,47 @@
package com.metis.enums;
import org.springframework.http.HttpStatus;
/**
* @author Clay
* @date 2023-05-10
*/
public enum ResultEnum {
/**
* 返回状态枚举
*/
SUCCESS(1000, "操作成功", HttpStatus.OK),
NO_DATA(1001, "查询结果为空", HttpStatus.OK),
RESUBMIT_LOCK(2002, "重复提交", HttpStatus.INTERNAL_SERVER_ERROR),
ERROR(2000, "操作失败", HttpStatus.INTERNAL_SERVER_ERROR),
SYS_ERROR(2001, "系统异常", HttpStatus.INTERNAL_SERVER_ERROR),
SENTINEL_FLOW(3000, "限流了", HttpStatus.INTERNAL_SERVER_ERROR),
SENTINEL_PARAM_FLOW(3000, "热点参数限流", HttpStatus.INTERNAL_SERVER_ERROR),
SENTINEL_SYSTEM(3000, "系统规则负载等不满足要求", HttpStatus.INTERNAL_SERVER_ERROR),
SENTINEL_AUTHORITY(3000, "授权规则不通过", HttpStatus.UNAUTHORIZED),
SENTINEL_DEGRADE(3000, "降级了", HttpStatus.INTERNAL_SERVER_ERROR),
;
ResultEnum(int code, String msg, HttpStatus status) {
this.code = code;
this.msg = msg;
this.status = status;
}
public final int code;
public final String msg;
public final transient HttpStatus status;
}

View File

@@ -0,0 +1,92 @@
package com.metis.handle;
import com.metis.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.security.auth.login.AccountExpiredException;
import java.nio.file.AccessDeniedException;
/**
* 全局异常处理器
*
* @author Clay
* @date 2022/10/30
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
public GlobalExceptionHandler() {
log.info("开始初始化全局异常处理器");
}
/**
* 路径不存在
*
* @param e
* @return
*/
@ExceptionHandler(NoHandlerFoundException.class)
public Result<Object> handlerNoFoundException(Exception e) {
log.error(e.getMessage(), e);
return Result.notFound( "路径不存在,请检查路径是否正确");
}
/**
* 授权失败
*
* @param e
* @return
*/
@ExceptionHandler(AccessDeniedException.class)
public Result<String> handleAuthorizationException(AccessDeniedException e) {
log.error(e.getMessage());
return Result.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权");
}
@ExceptionHandler(AccountExpiredException.class)
public Result<String> handleAccountExpiredException(AccountExpiredException e) {
log.error(e.getMessage(), e);
return Result.error(e.getMessage());
}
/**
* 自定义验证异常
*/
@ExceptionHandler(BindException.class)
public Result<String> validatedBindException(BindException e) {
log.error(e.getMessage(), e);
String message = e.getAllErrors().get(0).getDefaultMessage();
return Result.error(message);
}
/**
* 自定义验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<String> validExceptionHandler(MethodArgumentNotValidException e) {
log.error(e.getMessage(), e);
String message = e.getBindingResult().getFieldError().getDefaultMessage();
return Result.error(message);
}
@ExceptionHandler(RuntimeException.class)
public Result<String> runtimeExceptionHandler(RuntimeException e) {
log.error(e.getMessage(), e);
return Result.error(e.getMessage());
}
}

View File

@@ -0,0 +1,82 @@
package com.metis.mybatis;
import cn.hutool.core.net.NetUtil;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.core.injector.ISqlInjector;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.metis.mybatis.handler.BaseEntityMetaObjectHandler;
import com.metis.mybatis.support.CustomSqlInjector;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis-Plus配置
*
* @author ZhangQiang
* @date 2024/09/24
*/
@Configuration
public class MybatisPlusConfiguration {
/**
* mybatis-plus 拦截器
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 配置分页拦截器
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
paginationInnerInterceptor.setOptimizeJoin(false);
paginationInnerInterceptor.setMaxLimit(500L);
paginationInnerInterceptor.setOverflow(false);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
// 乐观锁插件
interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());
// 添加防止全表更新与删除插件
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}
/**
* 自动填充字段信息
*/
@Bean
public MetaObjectHandler metaObjectHandler() {
return new BaseEntityMetaObjectHandler();
}
/**
* 乐观锁插件
*/
public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor() {
return new OptimisticLockerInnerInterceptor();
}
/**
* 自定义SQL注入器
*
* @return 自定义SQL注入器
*/
@Bean
public ISqlInjector iSqlInjector() {
return new CustomSqlInjector();
}
/**
* 使用网卡信息绑定雪花生成器,实现集群生成id不重复
*/
@Bean
public IdentifierGenerator idGenerator() {
return new DefaultIdentifierGenerator(NetUtil.getLocalhost());
}
}

View File

@@ -0,0 +1,40 @@
package com.metis.mybatis.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import java.time.LocalDateTime;
/**
* 基础实体-自动填充处理器
*
* @author ZhangQiang
* @date 2024/09/29
*/
@Slf4j
public class BaseEntityMetaObjectHandler implements MetaObjectHandler {
/**
* 插入元对象字段填充(用于插入时对公共字段的填充)
*
* @param metaObject 元对象
*/
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "isDeleted", Integer.class, 0);
}
/**
* 更新元对象字段填充(用于更新时对公共字段的填充)
*
* @param metaObject 元对象
*/
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}

View File

@@ -0,0 +1,51 @@
package com.metis.mybatis.logic;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.toolkit.sql.SqlScriptUtils;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
/**
* 选择忽略逻辑删除
*
* @author clay
* @date 2024/12/02
*/
public class SelectIgnoreLogicDelete extends AbstractMethod {
public SelectIgnoreLogicDelete() {
super("SelectIgnoreLogicDelete");
}
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
SqlMethod sqlMethod = SqlMethod.SELECT_LIST;
String sql = String.format(sqlMethod.getSql(),
StrUtil.EMPTY,
sqlFirst(),
sqlSelectColumns(tableInfo, true),
tableInfo.getTableName(),
sqlWhereEntityWrapper(true, tableInfo),
sqlComment());
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addSelectMappedStatementForTable(mapperClass, "selectIgnoreLogicDelete", sqlSource, tableInfo);
}
@Override
protected String sqlWhereEntityWrapper(boolean newLine, TableInfo table) {
String sqlScript = table.getAllSqlWhere(false, true, true, WRAPPER_ENTITY_DOT);
sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", WRAPPER_ENTITY), true);
sqlScript += NEWLINE;
sqlScript += SqlScriptUtils.convertIf(String.format(SqlScriptUtils.convertIf(" AND", String.format("%s and %s", WRAPPER_NONEMPTYOFENTITY, WRAPPER_NONEMPTYOFNORMAL), false) + " ${%s}", WRAPPER_SQLSEGMENT),
String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT,
WRAPPER_NONEMPTYOFWHERE), true);
sqlScript = SqlScriptUtils.convertWhere(sqlScript) + NEWLINE;
sqlScript += SqlScriptUtils.convertIf(String.format(" ${%s}", WRAPPER_SQLSEGMENT),
String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT,
WRAPPER_EMPTYOFWHERE), true);
sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", WRAPPER), true);
return newLine ? NEWLINE + sqlScript : sqlScript;
}
}

View File

@@ -0,0 +1,25 @@
package com.metis.mybatis.support;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.metis.mybatis.logic.SelectIgnoreLogicDelete;
import org.apache.ibatis.session.Configuration;
import java.util.List;
/**
* 自定义SQL注入器
*
* @author clay
* @date 2024/12/02
*/
public class CustomSqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Configuration configuration, Class<?> mapperClass, TableInfo tableInfo) {
List<AbstractMethod> methodList = super.getMethodList(configuration, mapperClass, tableInfo);
// 查询所有数据(包括已经逻辑删除数据)
methodList.add(new SelectIgnoreLogicDelete());
return methodList;
}
}

View File

@@ -0,0 +1,149 @@
package com.metis.result;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.metis.enums.ResultEnum;
import org.springframework.http.HttpStatus;
import java.io.Serializable;
/**
* 返回结果集
*
* @author: Clay
* @date: 2022/5/31 10:00
*/
public class Result<T> implements Serializable {
private Integer code;
private String msg;
private T data;
private transient HttpStatus status;
public Result() {
}
public Result(Integer code, String msg, T data, HttpStatus status) {
this.code = code;
this.msg = msg;
this.data = data;
this.status = status;
}
public static <T> Result<T> ok(Integer code, String msg, T data) {
return new Result<>(code, msg, data, HttpStatus.OK);
}
public static <T> Result<T> ok(Integer code, String msg, T data, HttpStatus status) {
return new Result<>(code, msg, data, status);
}
public static <T> Result<T> ok(String msg, T data) {
return Result.ok(ResultEnum.SUCCESS.code, msg, data, ResultEnum.SUCCESS.status);
}
public static <T> Result<T> ok(Integer code, T data) {
return Result.ok(code, ResultEnum.SUCCESS.msg, data);
}
public static <T> Result<T> ok(String msg) {
return ok(ResultEnum.SUCCESS.code, msg, null);
}
public static <T> Result<T> ok(T data) {
return Result.ok(ResultEnum.SUCCESS.msg, data);
}
public static <T> Result<T> ok() {
return Result.ok(ResultEnum.SUCCESS.msg, null);
}
public static <T> Result<T> error(String msg, T data) {
return Result.error(ResultEnum.ERROR.code, msg, data);
}
public static <T> Result<T> error(Integer code, String msg) {
return Result.error(code, msg, null);
}
public static <T> Result<T> notFound(String msg) {
return Result.error(HttpStatus.NOT_FOUND.value(), msg, null, HttpStatus.NOT_FOUND);
}
public static <T> Result<T> error(Integer code, String msg, T data) {
return new Result<>(code, msg, data, HttpStatus.INTERNAL_SERVER_ERROR);
}
public static <T> Result<T> error(Integer code, String msg, T data, HttpStatus status) {
return new Result<>(code, msg, data, status);
}
public static <T> Result<T> unauthorized(String msg) {
return new Result<>(HttpStatus.UNAUTHORIZED.value(), msg, null, HttpStatus.UNAUTHORIZED);
}
public static <T> Result<T> error(HttpStatus status, String msg) {
return new Result<>(status.value(), msg, null, status);
}
public static <T> Result<T> error(ResultEnum resultEnum) {
return Result.error(resultEnum.code, resultEnum.msg, null, resultEnum.status);
}
public static <T> Result<T> error(String msg) {
return Result.error(ResultEnum.ERROR.code, msg, null, ResultEnum.ERROR.status);
}
public static <T> Result<T> error() {
return Result.error(ResultEnum.ERROR.code, ResultEnum.ERROR.msg, null);
}
public static <T> Result<T> info(String msg) {
return Result.ok(ResultEnum.NO_DATA.code, msg, null);
}
public static <T> Result<T> info(ResultEnum resultEnum) {
return Result.error(resultEnum.code, resultEnum.msg, null, resultEnum.status);
}
public static <T> Result<T> noData() {
return Result.ok(ResultEnum.NO_DATA.code, ResultEnum.NO_DATA.msg, null);
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
@JsonInclude(JsonInclude.Include.NON_NULL)
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
@JsonIgnore
public HttpStatus getStatus() {
return status;
}
@JsonIgnore
public void setStatus(HttpStatus status) {
this.status = status;
}
}

View File

@@ -0,0 +1,45 @@
package com.metis.result.page;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 表格分页数据对象
*
* @author binlin
*/
@Data
public class TableDataInfo<T> implements Serializable {
/**
* 总记录数
*/
private long total;
/**
* 列表数据
*/
private List<T> rows;
/**
* 表格数据对象
*/
public TableDataInfo() {
}
/**
* 分页
*
* @param list 列表数据
* @param total 总记录数
*/
public TableDataInfo(List<T> list, int total) {
this.rows = list;
this.total = total;
}
}

View File

@@ -0,0 +1,119 @@
package com.metis.sseclient;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.model.chat.request.json.JsonArraySchema;
import dev.langchain4j.model.chat.request.json.JsonBooleanSchema;
import dev.langchain4j.model.chat.request.json.JsonEnumSchema;
import dev.langchain4j.model.chat.request.json.JsonIntegerSchema;
import dev.langchain4j.model.chat.request.json.JsonNumberSchema;
import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
import dev.langchain4j.model.chat.request.json.JsonSchemaElement;
import dev.langchain4j.model.chat.request.json.JsonStringSchema;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class ToolSpecificationHelper {
/**
* Converts the 'tools' element from a ListToolsResult MCP message
* to a list of ToolSpecification objects.
*/
public static List<ToolSpecification> toolSpecificationListFromMcpResponse(ArrayNode array) {
List<ToolSpecification> result = new ArrayList<>();
for (JsonNode tool : array) {
final ToolSpecification.Builder builder = ToolSpecification.builder();
builder.name(tool.get("name").asText());
if (tool.has("description")) {
builder.description(tool.get("description").asText());
}
builder.parameters((JsonObjectSchema) jsonNodeToJsonSchemaElement(tool.get("inputSchema")));
result.add(builder.build());
}
return result;
}
/**
* Converts the 'inputSchema' element (inside the 'Tool' type in the MCP schema)
* to a JsonSchemaElement object that describes the tool's arguments.
*/
public static JsonSchemaElement jsonNodeToJsonSchemaElement(JsonNode node) {
String nodeType = node.get("type").asText();
if (nodeType.equals("object")) {
JsonObjectSchema.Builder builder = JsonObjectSchema.builder();
JsonNode required = node.get("required");
if (required != null) {
builder.required(toStringArray((ArrayNode) required));
}
if (node.has("additionalProperties")) {
builder.additionalProperties(node.get("additionalProperties").asBoolean(false));
}
JsonNode description = node.get("description");
if (description != null) {
builder.description(description.asText());
}
JsonNode properties = node.get("properties");
if (properties != null) {
ObjectNode propertiesObject = (ObjectNode) properties;
for (Map.Entry<String, JsonNode> property : propertiesObject.properties()) {
builder.addProperty(property.getKey(), jsonNodeToJsonSchemaElement(property.getValue()));
}
}
return builder.build();
} else if (nodeType.equals("string")) {
if (node.has("enum")) {
JsonEnumSchema.Builder builder = JsonEnumSchema.builder();
if (node.has("description")) {
builder.description(node.get("description").asText());
}
builder.enumValues(toStringArray((ArrayNode) node.get("enum")));
return builder.build();
} else {
JsonStringSchema.Builder builder = JsonStringSchema.builder();
if (node.has("description")) {
builder.description(node.get("description").asText());
}
return builder.build();
}
} else if (nodeType.equals("number")) {
JsonNumberSchema.Builder builder = JsonNumberSchema.builder();
if (node.has("description")) {
builder.description(node.get("description").asText());
}
return builder.build();
} else if (nodeType.equals("integer")) {
JsonIntegerSchema.Builder builder = JsonIntegerSchema.builder();
if (node.has("description")) {
builder.description(node.get("description").asText());
}
return builder.build();
} else if (nodeType.equals("boolean")) {
JsonBooleanSchema.Builder builder = JsonBooleanSchema.builder();
if (node.has("description")) {
builder.description(node.get("description").asText());
}
return builder.build();
} else if (nodeType.equals("array")) {
JsonArraySchema.Builder builder = JsonArraySchema.builder();
if (node.has("description")) {
builder.description(node.get("description").asText());
}
builder.items(jsonNodeToJsonSchemaElement(node.get("items")));
return builder.build();
} else {
throw new IllegalArgumentException("Unknown element type: " + nodeType);
}
}
private static String[] toStringArray(ArrayNode jsonArray) {
String[] result = new String[jsonArray.size()];
for (int i = 0; i < jsonArray.size(); i++) {
result[i] = jsonArray.get(i).asText();
}
return result;
}
}

View File

@@ -0,0 +1,180 @@
package com.metis.sseclient.check;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.metis.sseclient.ToolSpecificationHelper;
import com.metis.sseclient.event.SseEventListener;
import com.metis.sseclient.handler.McpOperationHandler;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.mcp.client.protocol.*;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import okhttp3.sse.EventSource;
import okhttp3.sse.EventSources;
import java.io.IOException;
import java.net.URI;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@Slf4j
public class SseCheck {
private final String sseUrl;
private volatile String postUrl;
private final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final AtomicLong idGenerator = new AtomicLong(0);
private final OkHttpClient client;
private EventSource eventSource;
private final String clientVersion = "1.0";
private final String clientName = "metis";
private final String protocolVersion = "2024-11-05";
public SseCheck(String sseUrl) {
this.sseUrl = sseUrl;
OkHttpClient.Builder httpClientBuilder = getHttpClientBuilder();
this.client = httpClientBuilder.build();
initialize();
}
public void start() {
Map<Long, CompletableFuture<JsonNode>> pendingOperations = new ConcurrentHashMap<>();
Request request = new Request.Builder().url(sseUrl).build();
CompletableFuture<String> initializationFinished = new CompletableFuture<>();
McpOperationHandler operationHandler = new McpOperationHandler(pendingOperations);
SseEventListener listener = new SseEventListener(operationHandler, true, initializationFinished);
this.eventSource = EventSources.createFactory(client).newEventSource(request, listener);
// wait for the SSE channel to be created, receive the POST url from the server, throw an exception if that
// failed
try {
int timeout = client.callTimeoutMillis() > 0 ? client.callTimeoutMillis() : Integer.MAX_VALUE;
String relativePostUrl = initializationFinished.get(timeout, TimeUnit.MILLISECONDS);
postUrl = buildAbsolutePostUrl(relativePostUrl);
log.debug("Received the server's POST URL: {}", postUrl);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private OkHttpClient.Builder getHttpClientBuilder() {
OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder();
Duration timeout = Duration.ofSeconds(60);
httpClientBuilder.callTimeout(timeout);
httpClientBuilder.connectTimeout(timeout);
httpClientBuilder.readTimeout(timeout);
httpClientBuilder.writeTimeout(timeout);
return httpClientBuilder;
}
private String buildAbsolutePostUrl(String relativePostUrl) {
try {
return URI.create(sseUrl).resolve(relativePostUrl).toString();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void initialize() {
start();
long operationId = idGenerator.getAndIncrement();
McpInitializeRequest request = new McpInitializeRequest(operationId);
InitializeParams params = createInitializeParams();
request.setParams(params);
try {
Request httpRequest = null;
Request initializationNotification = null;
try {
httpRequest = createRequest(request);
initializationNotification = createRequest(new InitializationNotification());
} catch (JsonProcessingException e) {
return;
}
final Request finalInitializationNotification = initializationNotification;
execute(httpRequest, operationId)
.thenCompose(originalResponse -> execute(finalInitializationNotification, null)
.thenCompose(nullNode -> CompletableFuture.completedFuture(originalResponse)));
// log.debug("MCP server capabilities: {}", capabilities.get("result"));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private InitializeParams createInitializeParams() {
InitializeParams params = new InitializeParams();
params.setProtocolVersion(protocolVersion);
InitializeParams.ClientInfo clientInfo = new InitializeParams.ClientInfo();
clientInfo.setName(clientName);
clientInfo.setVersion(clientVersion);
params.setClientInfo(clientInfo);
InitializeParams.Capabilities capabilities = new InitializeParams.Capabilities();
InitializeParams.Capabilities.Roots roots = new InitializeParams.Capabilities.Roots();
roots.setListChanged(false); // TODO: listChanged is not supported yet
capabilities.setRoots(roots);
params.setCapabilities(capabilities);
return params;
}
public List<ToolSpecification> listTools() throws JsonProcessingException {
McpListToolsRequest operation = new McpListToolsRequest(idGenerator.getAndIncrement());
Request request = createRequest(operation);
CompletableFuture<JsonNode> resultFuture = execute(request, idGenerator.getAndIncrement());
JsonNode result = null;
try {
result = resultFuture.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
return ToolSpecificationHelper.toolSpecificationListFromMcpResponse(
(ArrayNode) result.get("result").get("tools"));
}
private Request createRequest(McpClientMessage message) throws JsonProcessingException {
return new Request.Builder()
.url(postUrl)
.header("Content-Type", "application/json")
.post(RequestBody.create(OBJECT_MAPPER.writeValueAsBytes(message)))
.build();
}
private CompletableFuture<JsonNode> execute(Request request, Long id) {
CompletableFuture<JsonNode> future = new CompletableFuture<>();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
future.completeExceptionally(e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
int statusCode = response.code();
if (!isExpectedStatusCode(statusCode)) {
future.completeExceptionally(new RuntimeException("Unexpected status code: " + statusCode));
}
// For messages with null ID, we don't wait for a response in the SSE channel
if (id == null) {
future.complete(null);
}
}
});
return future;
}
private boolean isExpectedStatusCode(int statusCode) {
return statusCode >= 200 && statusCode < 300;
}
}

View File

@@ -0,0 +1,75 @@
package com.metis.sseclient.event;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.metis.sseclient.handler.McpOperationHandler;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Response;
import okhttp3.sse.EventSource;
import okhttp3.sse.EventSourceListener;
import java.util.concurrent.CompletableFuture;
@Slf4j
public class SseEventListener extends EventSourceListener {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final boolean logEvents;
// this will contain the POST url for sending commands to the server
private final CompletableFuture<String> initializationFinished;
private final McpOperationHandler messageHandler;
public SseEventListener(
McpOperationHandler messageHandler, boolean logEvents, CompletableFuture initializationFinished) {
this.messageHandler = messageHandler;
this.logEvents = logEvents;
this.initializationFinished = initializationFinished;
}
@Override
public void onClosed(EventSource eventSource) {
log.debug("SSE channel closed");
}
@Override
public void onEvent(EventSource eventSource, String id, String type, String data) {
if (type.equals("message")) {
if (logEvents) {
log.info("< {}", data);
}
try {
JsonNode jsonNode = OBJECT_MAPPER.readTree(data);
messageHandler.handle(jsonNode);
} catch (JsonProcessingException e) {
log.warn("Failed to parse JSON message: {}", data, e);
}
} else if (type.equals("endpoint")) {
if (initializationFinished.isDone()) {
log.warn("Received endpoint event after initialization");
return;
}
initializationFinished.complete(data);
}
}
@Override
public void onFailure(EventSource eventSource, Throwable t, Response response) {
if (!initializationFinished.isDone()) {
if (t != null) {
initializationFinished.completeExceptionally(t);
} else if (response != null) {
initializationFinished.completeExceptionally(
new RuntimeException("The server returned: " + response.message()));
}
}
if (t != null && (t.getMessage() == null || !t.getMessage().contains("Socket closed"))) {
log.warn("SSE channel failure", t);
}
}
@Override
public void onOpen(EventSource eventSource, Response response) {
log.debug("Connected to SSE channel at {}", response.request().url());
}
}

View File

@@ -0,0 +1,56 @@
package com.metis.sseclient.handler;
import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
/**
* Handles incoming messages from the MCP server. Transport implementations
* should call the "handle" method on each received message. A transport also has
* to call "startOperation" when before starting an operation that requires a response
* to register its ID in the map of pending operations.
*/
public class McpOperationHandler {
private final Map<Long, CompletableFuture<JsonNode>> pendingOperations;
private static final Logger log = LoggerFactory.getLogger(McpOperationHandler.class);
public McpOperationHandler(
Map<Long, CompletableFuture<JsonNode>> pendingOperations) {
this.pendingOperations = pendingOperations;
}
public void handle(JsonNode message) {
if (message.has("id")) {
long messageId = message.get("id").asLong();
CompletableFuture<JsonNode> op = pendingOperations.remove(messageId);
if (op != null) {
op.complete(message);
} else {
if (message.has("method")) {
String method = message.get("method").asText();
if (method.equals("ping")) {
return;
}
}
log.warn("Received response for unknown message id: {}", messageId);
}
} else if (message.has("method") && message.get("method").asText().equals("notifications/message")) {
// this is a log message
if (message.has("params")) {
log.info("Received log message: {}", message);
} else {
log.warn("Received log message without params: {}", message);
}
} else {
log.warn("Received unknown message: {}", message);
}
}
public void startOperation(Long id, CompletableFuture<JsonNode> future) {
pendingOperations.put(id, future);
}
}

View File

@@ -0,0 +1,399 @@
package com.metis.utils;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAdjusters;
import java.time.temporal.TemporalUnit;
import java.util.Date;
/**
* 地方日期时间工具类
*
* @author ZhangQiang
* @date 2024/10/28
*/
public class LocalDateTimeUtils extends cn.hutool.core.date.LocalDateTimeUtil {
/**
* 在范围内
*
* @param target 目标
* @param start 开始
* @param end 结束
* @return boolean
*/
public static boolean isWithinRange(LocalDateTime target, LocalDateTime start, LocalDateTime end) {
return !target.isBefore(start) && !target.isAfter(end);
}
/**
* 现在时间在范围内
*
* @param start 开始
* @param end 结束
* @return boolean
*/
public static boolean nowIsWithinRange(LocalDateTime start, LocalDateTime end) {
LocalDateTime target = LocalDateTime.now();
return !target.isBefore(start) && !target.isAfter(end);
}
/**
* 获取指定时间是周几
*
* @param time 时间
* @return int
*/
public static int week(LocalDateTime time) {
return time.getDayOfWeek().getValue();
}
/**
* 获取加或减N月的第一天
*
* @param num 数字
* @return {@link LocalDateTime}
*/
public static LocalDateTime monthFirst(int num) {
LocalDateTime newTime = plus(LocalDateTime.now(), num, ChronoUnit.MONTHS);
newTime = newTime.with(TemporalAdjusters.firstDayOfMonth());
return getDayStart(newTime);
}
/**
* 获取加或减N月的最后天
*
* @param num 数字
* @return {@link LocalDateTime}
*/
public static LocalDateTime monthLast(int num) {
LocalDateTime newTime = plus(LocalDateTime.now(), num, ChronoUnit.MONTHS);
newTime = newTime.with(TemporalAdjusters.lastDayOfMonth());
return getDayEnd(newTime);
}
/**
* 获取加或减N周的第一天
*
* @param num 数字
* @return {@link LocalDateTime}
*/
public static LocalDateTime weekFirst(int num) {
int week = week(LocalDateTime.now());
LocalDateTime newTime = subtract(LocalDateTime.now(), week - 1, ChronoUnit.DAYS);
newTime = plus(newTime, num * 7L, ChronoUnit.DAYS);
return getDayStart(newTime);
}
/**
* 获取加或减N周的最后一天
*
* @param num 数字
* @return {@link LocalDateTime}
*/
public static LocalDateTime weekLast(int num) {
int week = week(LocalDateTime.now());
LocalDateTime newTime = plus(LocalDateTime.now(), 7 - week, ChronoUnit.DAYS);
newTime = plus(newTime, num * 7L, ChronoUnit.DAYS);
return getDayEnd(newTime);
}
/**
* 判断时间 ==> t1 < t2 = true 2019-10-13 11:11:00 < 2020-11-13 13:13:00 = true
*
* @param t1 t1
* @param t2 t2
* @return boolean
*/
public static boolean isBefore(LocalDateTime t1, LocalDateTime t2) {
return t1.isBefore(t2);
}
/**
* 判断时间 ==> t1 > t2 = true2019-10-13 11:11:00 > 2020-11-13 13:13:00 = false
*
* @param t1 t1
* @param t2 t2
* @return boolean
*/
public static boolean isAfter(LocalDateTime t1, LocalDateTime t2) {
return t1.isAfter(t2);
}
/**
* Date 转 LocalDateTime
*
* @param date 日期
* @return {@link LocalDateTime}
*/
public static LocalDateTime convertToLocalDateTime(Date date) {
return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
}
/**
* LocalDateTime 转 Date
*
* @param time 时间
* @return {@link Date}
*/
public static Date convertToDate(LocalDateTime time) {
return Date.from(time.atZone(ZoneId.systemDefault()).toInstant());
}
/**
* Date转LocalDate
*
* @param date 日期
* @return {@link LocalDate}
*/
public static LocalDate convertLocalDate(Date date) {
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
}
/**
* Date转LocalDate
*
* @param date 日期
* @return {@link LocalTime}
*/
public static LocalTime convertLocalTime(Date date) {
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalTime();
}
/**
* 获取指定日期的毫秒
*
* @param time 时间
* @return {@link Long}
*/
public static Long getMilliByTime(LocalDateTime time) {
return time.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
}
/**
* 获取秒数通过时间
* 获取指定日期的秒
*
* @param time 时间
* @return {@link Long}
*/
public static Long getSecondsByTime(LocalDateTime time) {
return time.atZone(ZoneId.systemDefault()).toInstant().getEpochSecond();
}
/**
* 获取指定时间的指定格式 ==> yyyy-MM-dd HH:mm:ss:SSS (HH是24小时制而hh是12小时制, ss是秒SSS是毫秒)
*
* @param time 时间
* @param pattern 图案
* @return {@link String}
*/
public static String formatTime(LocalDateTime time, String pattern) {
return time.format(DateTimeFormatter.ofPattern(pattern));
}
/**
* 日期加上一个数,根据field不同加不同值,field为ChronoUnit.*
*
* @param time 时间
* @param number 数
* @param field 领域
* @return {@link LocalDateTime}
*/
public static LocalDateTime plus(LocalDateTime time, long number, TemporalUnit field) {
return time.plus(number, field);
}
/**
* 减去
* 日期减去一个数,根据field不同减不同值,field参数为ChronoUnit.*
*
* @param time 时间
* @param number 数
* @param field 领域
* @return {@link LocalDateTime}
*/
public static LocalDateTime subtract(LocalDateTime time, long number, TemporalUnit field) {
return time.minus(number, field);
}
/**
* 获取白天开始
* 获取指定某一天的开始时间 00:00:00
*
* @param time 时间
* @return {@link LocalDateTime}
*/
public static LocalDateTime getDayStart(LocalDateTime time) {
return time.withHour(0)
.withMinute(0)
.withSecond(0)
.withNano(0);
}
/**
* 获取白天结束
* 获取指定某一天的结束时间 23:59:59.999
*
* @param time 时间
* @return {@link LocalDateTime}
*/
public static LocalDateTime getDayEnd(LocalDateTime time) {
return time.withHour(23)
.withMinute(59)
.withSecond(59)
.withNano(999999999);
}
/**
* 获取本周周一
*
* @param time 时间
* @return {@link LocalDateTime}
*/
public static LocalDateTime getWeekOfFirst(LocalDateTime time) {
return time.with(TemporalAdjusters.previous(DayOfWeek.SUNDAY)).
plusDays(1).withHour(0).withMinute(0).withSecond(0);
}
/**
* 获取本周周日
*
* @param time 时间
* @return {@link LocalDateTime}
*/
public static LocalDateTime getWeekOfLast(LocalDateTime time) {
return time.with(TemporalAdjusters.next(DayOfWeek.MONDAY)).
minusDays(1).withHour(23).withMinute(59).withSecond(59);
}
/**
* 获取本月第一天
*
* @param time 时间
* @return {@link LocalDateTime}
*/
public static LocalDateTime getMonthOfFirst(LocalDateTime time) {
LocalDateTime firsthand = time.with(TemporalAdjusters.firstDayOfMonth());
return LocalDateTime.of(firsthand.toLocalDate(), LocalTime.MIN);
}
/**
* 获取本月最后一天
*
* @param time 时间
* @return {@link LocalDateTime}
*/
public static LocalDateTime getMonthOfLast(LocalDateTime time) {
LocalDateTime lastDay = time.with(TemporalAdjusters.lastDayOfMonth());
return LocalDateTime.of(lastDay.toLocalDate(), LocalTime.MAX);
}
/**
* 日期相隔天数
*
* @param startDateInclusive 开始日期(含)
* @param endDateExclusive 结束日期除外
* @return int
*/
public static int periodDays(LocalDate startDateInclusive, LocalDate endDateExclusive) {
return Period.between(startDateInclusive, endDateExclusive).getDays();
}
/**
* 日期相隔小时
*
* @param startInclusive 开始包容性
* @param endExclusive 结束独家
* @return long
*/
public static long durationHours(Temporal startInclusive, Temporal endExclusive) {
return Duration.between(startInclusive, endExclusive).toHours();
}
/**
* 日期相隔分钟
*
* @param startInclusive 开始包容性
* @param endExclusive 结束独家
* @return long
*/
public static long durationMinutes(Temporal startInclusive, Temporal endExclusive) {
return Duration.between(startInclusive, endExclusive).toMinutes();
}
/**
* 持续时间秒
*
* @param startInclusive 开始包容
* @param endExclusive 结束独家
* @return long
*/
public static long durationSeconds(Temporal startInclusive, Temporal endExclusive) {
return Duration.between(startInclusive, endExclusive).toSeconds();
}
/**
* 日期相隔毫秒数
*
* @param startInclusive 开始包容性
* @param endExclusive 结束独家
* @return long
*/
public static long durationMillis(Temporal startInclusive, Temporal endExclusive) {
return Duration.between(startInclusive, endExclusive).toMillis();
}
/**
* 是否当天
*/
public static boolean isToday(LocalDate date) {
return LocalDate.now().equals(date);
}
/**
* 判断两个时间是否相差指定时间(精确到毫秒)
*
* @param time1 第一个时间点
* @param time2 第二个时间点
* @param milliseconds 指定时间差(毫秒)
* @return true 如果两个时间相差超过指定毫秒数,否则 false
*/
public static boolean isTimeDifferenceExceeds(LocalDateTime time1, LocalDateTime time2, long milliseconds) {
if (time1 == null || time2 == null) {
throw new IllegalArgumentException("Time arguments cannot be null");
}
Duration duration = Duration.between(time1, time2);
return Math.abs(duration.toMillis()) > milliseconds;
}
/**
* 判断指定时间是否与当前时间相差超过 24 小时(精确到毫秒)
*
* @param targetTime 指定的 LocalDateTime 时间
* @return true 如果相差 24 小时以上,否则 false
*/
public static boolean isMoreThan24HoursApart(LocalDateTime targetTime) {
if (targetTime == null) {
throw new IllegalArgumentException("Target time cannot be null");
}
// 比较时间差的绝对值是否大于 24 小时
return isTimeDifferenceExceeds(LocalDateTime.now(), targetTime, 86400000L);
}
}

View File

@@ -0,0 +1,66 @@
package com.metis.utils;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.metis.result.page.TableDataInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* @author Clay
* @date 2023-05-25
*/
public class PageConditionUtil {
/**
* 转换为TableDataInfo对象
*
* @param page 源对象
* @param map 转换方法
* @param <T> 转换后的对象类型
* @param <R> 需要转换的对象类型
* @return 转换后的对象
*/
public static <T, R> TableDataInfo<T> convertDataTable(IPage<R> page, Function<R, T> map) {
List<T> convertList = page.getRecords().stream().map(map).collect(Collectors.toList());
return convertDataTable(convertList, page.getTotal());
}
/**
* 转换为TableDataInfo对象
*
* @param page 源对象
* @return 转换后的对象
*/
public static <T> TableDataInfo<T> convertDataTable(IPage<T> page) {
return convertDataTable(page.getRecords(), page.getTotal());
}
/**
* 转换为TableDataInfo对象
*
* @param list
* @param count
* @param <T>
* @return
*/
public static <T> TableDataInfo<T> convertDataTable(List<T> list, Long count) {
if (null == list) {
return new TableDataInfo<>(new ArrayList<>(), 0);
}
TableDataInfo<T> tableDataInfo = new TableDataInfo<>();
tableDataInfo.setRows(list);
tableDataInfo.setTotal(count);
return tableDataInfo;
}
public static <T> IPage<T> getPage() {
PageInfo pageInfo = TableSupport.getPageInfo();
return new Page<>(pageInfo.getPageNum(), pageInfo.getPageSize());
}
}

View File

@@ -0,0 +1,44 @@
package com.metis.utils;
import cn.hutool.core.util.StrUtil;
import lombok.Data;
/**
* @author Clay
* @date 2022/10/30
*/
@Data
public class PageInfo {
/**
* 当前记录起始索引
*/
private Integer pageNum;
/**
* 每页显示记录数
*/
private Integer pageSize;
/**
* 排序列
*/
private String orderByColumn;
/**
* 排序的方向desc或者asc
*/
private String isAsc = "asc";
/**
* 分页参数合理化
*/
private Boolean reasonable = true;
public String getOrderBy() {
if (StrUtil.isEmpty(orderByColumn)) {
return "";
}
return StrUtil.toUnderlineCase(orderByColumn) + " " + isAsc;
}
}

View File

@@ -0,0 +1,79 @@
package com.metis.utils;
import cn.hutool.core.convert.Convert;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* 表格数据处理
*
* @author Clay
* @date 2022/10/30
*/
public class TableSupport {
/**
* 当前记录起始索引
*/
public static final String PAGE_NUM = "pageNum";
/**
* 每页显示记录数
*/
public static final String PAGE_SIZE = "pageSize";
/**
* 排序列
*/
public static final String ORDER_BY_COLUMN = "orderByColumn";
/**
* 排序的方向 "desc" 或者 "asc".
*/
public static final String IS_ASC = "isAsc";
/**
* 分页参数合理化
*/
public static final String REASONABLE = "reasonable";
/**
* 封装分页对象
*/
public static PageInfo getPageInfo() {
PageInfo pageInfo = new PageInfo();
pageInfo.setPageNum(Convert.toInt(getParameter(PAGE_NUM), 1));
pageInfo.setPageSize(Convert.toInt(getParameter(PAGE_SIZE), 10));
pageInfo.setOrderByColumn(getParameter(ORDER_BY_COLUMN));
pageInfo.setIsAsc(getParameter(IS_ASC));
pageInfo.setReasonable(getParameterToBool(REASONABLE));
return pageInfo;
}
public static PageInfo buildPageRequest() {
return getPageInfo();
}
/**
* 获取Boolean参数
*/
public static Boolean getParameterToBool(String name) {
return Convert.toBool(getRequest().getParameter(name));
}
/**
* 获取String参数
*/
public static String getParameter(String name) {
return Convert.toStr(getRequest().getParameter(name));
}
public static HttpServletRequest getRequest() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return attributes.getRequest();
}
}

View File

@@ -0,0 +1,43 @@
package com.metis.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
@Slf4j
public class TransactionalUtils {
/**
* 提交后执行
*
* @param runnable 可运行
*/
public static void afterCommitExecute(Runnable runnable) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
log.info("事务提交后执行业务");
runnable.run();
}
});
}
/**
* 事务回滚时执行
*
* @param runnable 可运行
*/
public static void afterRollbackExecute(Runnable runnable) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCompletion(int status) {
// 只在事务状态为 STATUS_ROLLED_BACK 时执行
if (status == STATUS_ROLLED_BACK) {
log.info("事务回滚后执行业务");
runnable.run();
}
}
});
}
}

View File

@@ -0,0 +1,20 @@
# Spring配置
spring:
datasource:
url: jdbc:mysql://frp.feashow.cn:39306/metis?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: yyz@2024
driver-class-name: com.mysql.cj.jdbc.Driver
data:
redis:
host: frp.feashow.cn
port: 39379
password: yyz@2024
database: 13
timeout: 10s
lettuce:
pool:
min-idle: 0
max-idle: 8
max-active: 8
max-wait: -1ms

View File

@@ -0,0 +1,7 @@
server:
port: 8080
spring:
application:
name: metis

View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SSE Demo</title>
</head>
<body>
<h1>SSE 连接示例</h1>
<div id="messages"></div>
<script> // 创建一个新的EventSource对象连接到SSE服务
const eventSource = new EventSource('http://localhost:8081/sse');
// 监听服务器发送的消息事件
eventSource.onmessage = function(event) {
const message = document.createElement('p');
message.textContent = '收到消息: ' + event.data;
document.getElementById('messages').appendChild(message);
};
// 监听自定义事件(如果有)
eventSource.addEventListener('customEvent', function(event) {
const message = document.createElement('p');
message.textContent = '收到自定义事件: ' + event.data;
document.getElementById('messages').appendChild(message);
});
// 监听连接打开事件
eventSource.onopen = function(event) {
const message = document.createElement('p');
message.textContent = 'SSE连接已建立';
document.getElementById('messages').appendChild(message);
};
// 监听错误事件
eventSource.onerror = function(event) {
const message = document.createElement('p');
message.textContent = 'SSE连接发生错误';
document.getElementById('messages').appendChild(message);
// 在这里处理错误,例如尝试重新连接
};
</script>
</body>
</html>

View File

@@ -0,0 +1,8 @@
import java.io.InputStream;
public interface AnsMsgHandler {
void actMsg(InputStream is, String line);
}

View File

@@ -0,0 +1,16 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.metis.sseclient.check.SseCheck;
import dev.langchain4j.agent.tool.ToolSpecification;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
@Slf4j
public class SSeTest {
public static void main(String[] args) throws JsonProcessingException {
SseCheck sseCheck = new SseCheck("http://localhost:8081/sse");
List<ToolSpecification> listTools = sseCheck.listTools();
System.out.println(listTools);
}
}

View File

@@ -0,0 +1,77 @@
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
/**
*
* @author hyd
*
*/
public class SseClient {
/**
* 获取SSE输入流。
*
* @param urlPath
* @return
* @throws IOException
*/
public static InputStream getSseInputStream(String urlPath) throws IOException {
URL url = new URL(urlPath);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
// 这儿根据自己的情况选择get或post
urlConnection.setRequestMethod("GET");
urlConnection.setDoOutput(true);
urlConnection.setDoInput(true);
urlConnection.setUseCaches(false);
urlConnection.setRequestProperty("Connection", "Keep-Alive");
urlConnection.setRequestProperty("Charset", "UTF-8");
//读取过期时间(很重要,建议加上)
urlConnection.setReadTimeout(60 * 1000);
// text/plain模式
urlConnection.setRequestProperty("Content-Type", "text/plain; charset=UTF-8");
InputStream inputStream = urlConnection.getInputStream();
InputStream is = new BufferedInputStream(inputStream);
return is;
}
/**
* 读取数据。
*
* @param is
* @param ansMsgHandler
* @throws IOException
*/
public static void readStream(InputStream is, AnsMsgHandler ansMsgHandler) throws IOException {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line = "";
while ((line = reader.readLine()) != null) {
// 处理数据接口
ansMsgHandler.actMsg(is, line);
}
// 当服务器端主动关闭的时候,客户端无法获取到信号。现在还不清楚原因。所以无法执行的此处。
reader.close();
} catch (IOException e) {
e.printStackTrace();
throw new IOException("关闭数据流!");
}
}
public static void main(String[] args) throws IOException {
String urlPath = "http://localhost:8081/sse";
InputStream inputStream = getSseInputStream(urlPath);
readStream(inputStream, new AnsMsgHandler() {
public void actMsg(InputStream is, String line) {
System.out.println(line);
}
});
}
}