diff --git a/himarket-bootstrap/Dockerfile b/himarket-bootstrap/Dockerfile index 5a5eef4f2..a063afaf3 100644 --- a/himarket-bootstrap/Dockerfile +++ b/himarket-bootstrap/Dockerfile @@ -8,4 +8,4 @@ EXPOSE 8080 # Support reflection in java 17 ENV JAVA_OPTS="--add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED" -ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar --logging.file.name=/app/logs/himarket-server.log"] \ No newline at end of file +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar --spring.profiles.active=prod"] \ No newline at end of file diff --git a/himarket-bootstrap/src/main/java/com/alibaba/himarket/config/RestTemplateConfig.java b/himarket-bootstrap/src/main/java/com/alibaba/himarket/config/RestTemplateConfig.java deleted file mode 100644 index 0019309d2..000000000 --- a/himarket-bootstrap/src/main/java/com/alibaba/himarket/config/RestTemplateConfig.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package com.alibaba.himarket.config; - -import java.util.concurrent.TimeUnit; -import okhttp3.ConnectionPool; -import okhttp3.OkHttpClient; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.client.OkHttp3ClientHttpRequestFactory; -import org.springframework.web.client.RestTemplate; - -@Configuration -public class RestTemplateConfig { - - @Bean - public RestTemplate restTemplate(OkHttpClient okHttpClient) { - // 使用OkHttp作为RestTemplate的底层客户端 - return new RestTemplate(new OkHttp3ClientHttpRequestFactory(okHttpClient)); - } - - @Bean - public OkHttpClient okHttpClient() { - return new OkHttpClient.Builder() - .connectTimeout(5, TimeUnit.SECONDS) - .readTimeout(5, TimeUnit.SECONDS) - .writeTimeout(5, TimeUnit.SECONDS) - .connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES)) - .build(); - } -} diff --git a/himarket-bootstrap/src/main/java/com/alibaba/himarket/config/SwaggerConfig.java b/himarket-bootstrap/src/main/java/com/alibaba/himarket/config/SwaggerConfig.java index b07e3a7e1..fa5f9b0a8 100644 --- a/himarket-bootstrap/src/main/java/com/alibaba/himarket/config/SwaggerConfig.java +++ b/himarket-bootstrap/src/main/java/com/alibaba/himarket/config/SwaggerConfig.java @@ -29,6 +29,10 @@ public class SwaggerConfig { @Bean public OpenAPI openAPI() { return new OpenAPI() - .info(new Info().title("开放平台 API").version("1.0.0").description("API 文档描述")); + .info( + new Info() + .title("HiMarket Open API") + .version("1.0.0") + .description("API Document")); } } diff --git a/himarket-bootstrap/src/main/java/com/alibaba/himarket/config/WebMvcConfig.java b/himarket-bootstrap/src/main/java/com/alibaba/himarket/config/WebMvcConfig.java index 77c9e1b2a..6085d85a3 100644 --- a/himarket-bootstrap/src/main/java/com/alibaba/himarket/config/WebMvcConfig.java +++ b/himarket-bootstrap/src/main/java/com/alibaba/himarket/config/WebMvcConfig.java @@ -21,19 +21,35 @@ import java.util.List; import org.jetbrains.annotations.NotNull; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration -public class WebMvcConfig { +public class WebMvcConfig implements WebMvcConfigurer { - @Bean - public PageableHandlerMethodArgumentResolver pageableResolver() { + @Override + public void configureAsyncSupport(AsyncSupportConfigurer configurer) { + // Create a thread pool for async handling + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(10); + executor.setMaxPoolSize(50); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("async-"); + executor.initialize(); + + configurer.setTaskExecutor(executor); + configurer.setDefaultTimeout(30000); + } + + @Override + public void addArgumentResolvers(@NotNull List resolvers) { + // Add pageable resolver PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver(); // Default page size is 100 @@ -41,17 +57,7 @@ public PageableHandlerMethodArgumentResolver pageableResolver() { PageRequest.of(0, 100, Sort.by(Sort.Direction.DESC, "createAt"))); // Page index starts from 1 resolver.setOneIndexedParameters(true); - return resolver; - } - @Bean - public WebMvcConfigurer webMvcConfigurer() { - return new WebMvcConfigurer() { - @Override - public void addArgumentResolvers( - @NotNull List resolvers) { - resolvers.add(pageableResolver()); - } - }; + resolvers.add(resolver); } } diff --git a/himarket-bootstrap/src/main/resources/logback-spring.xml b/himarket-bootstrap/src/main/resources/logback-spring.xml index 2369f2566..d1bfb5136 100644 --- a/himarket-bootstrap/src/main/resources/logback-spring.xml +++ b/himarket-bootstrap/src/main/resources/logback-spring.xml @@ -1,21 +1,53 @@ - + - %d{yyyy-MM-dd HH:mm:ss.SSS} %magenta([%thread]) %highlight(%-5level) %cyan(%logger{50}) - %msg%n + %d{yyyy-MM-dd HH:mm:ss.SSS} %magenta([%thread]) %highlight(%-5level) %cyan(%logger{50}) - %msg%n + UTF-8 - - - + + + + /app/logs/himarket-server.log - + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + + + /app/logs/archived/himarket-server-%d{yyyy-MM-dd}.%i.log.gz + 100MB + 30 + 3GB + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/entity/Administrator.java b/himarket-dal/src/main/java/com/alibaba/himarket/entity/Administrator.java index ef75b058b..9bbf0d96c 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/entity/Administrator.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/entity/Administrator.java @@ -20,15 +20,8 @@ package com.alibaba.himarket.entity; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.*; -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder @Entity @Table( name = "administrator", @@ -40,6 +33,11 @@ columnNames = {"username"}, name = "uk_username") }) +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor public class Administrator extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/entity/Chat.java b/himarket-dal/src/main/java/com/alibaba/himarket/entity/Chat.java index 9e8c2213b..b9caf2636 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/entity/Chat.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/entity/Chat.java @@ -26,8 +26,7 @@ import com.alibaba.himarket.support.enums.ChatStatus; import jakarta.persistence.*; import java.util.List; -import lombok.Data; -import lombok.experimental.Accessors; +import lombok.*; @Entity @Table( @@ -38,7 +37,10 @@ name = "uk_chat_id") }) @Data -@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor public class Chat extends BaseEntity { @Id @@ -74,6 +76,7 @@ public class Chat extends BaseEntity { */ @Column(name = "status", length = 32) @Enumerated(EnumType.STRING) + @Builder.Default private ChatStatus status = ChatStatus.INIT; /** diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/entity/ChatAttachment.java b/himarket-dal/src/main/java/com/alibaba/himarket/entity/ChatAttachment.java index d4980bb26..53af7e8fe 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/entity/ChatAttachment.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/entity/ChatAttachment.java @@ -21,8 +21,7 @@ import com.alibaba.himarket.support.enums.ChatAttachmentType; import jakarta.persistence.*; -import lombok.Data; -import lombok.experimental.Accessors; +import lombok.*; import org.hibernate.annotations.ColumnDefault; @Entity @@ -34,7 +33,10 @@ name = "uk_attachment_id") }) @Data -@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor public class ChatAttachment extends BaseEntity { @Id diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/entity/ChatSession.java b/himarket-dal/src/main/java/com/alibaba/himarket/entity/ChatSession.java index 5f35c5e9f..d561eafff 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/entity/ChatSession.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/entity/ChatSession.java @@ -22,8 +22,7 @@ import com.alibaba.himarket.converter.ListJsonConverter; import jakarta.persistence.*; import java.util.List; -import lombok.Data; -import lombok.experimental.Accessors; +import lombok.*; @Entity @Table( @@ -34,7 +33,10 @@ name = "uk_session_id") }) @Data -@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor public class ChatSession extends BaseEntity { @Id diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/entity/Consumer.java b/himarket-dal/src/main/java/com/alibaba/himarket/entity/Consumer.java index e22fa88c0..bd06bc77d 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/entity/Consumer.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/entity/Consumer.java @@ -20,8 +20,7 @@ package com.alibaba.himarket.entity; import jakarta.persistence.*; -import lombok.Data; -import lombok.EqualsAndHashCode; +import lombok.*; @Entity @Table( @@ -36,6 +35,9 @@ }) @Data @EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor public class Consumer extends BaseEntity { @Id diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/entity/ConsumerCredential.java b/himarket-dal/src/main/java/com/alibaba/himarket/entity/ConsumerCredential.java index 7590eda95..b69be15ae 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/entity/ConsumerCredential.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/entity/ConsumerCredential.java @@ -26,7 +26,7 @@ import com.alibaba.himarket.support.consumer.HmacConfig; import com.alibaba.himarket.support.consumer.JwtConfig; import jakarta.persistence.*; -import lombok.Data; +import lombok.*; @Entity @Table( @@ -37,6 +37,10 @@ name = "uk_consumer_id") }) @Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor public class ConsumerCredential extends BaseEntity { @Id diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/entity/ConsumerRef.java b/himarket-dal/src/main/java/com/alibaba/himarket/entity/ConsumerRef.java index a23ce4988..a6cedddc7 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/entity/ConsumerRef.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/entity/ConsumerRef.java @@ -28,10 +28,10 @@ @Entity @Table(name = "consumer_ref") @Data +@EqualsAndHashCode(callSuper = true) @Builder -@AllArgsConstructor @NoArgsConstructor -@EqualsAndHashCode(callSuper = true) +@AllArgsConstructor public class ConsumerRef extends BaseEntity { @Id diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/entity/Developer.java b/himarket-dal/src/main/java/com/alibaba/himarket/entity/Developer.java index da5ed57f1..06b2ce576 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/entity/Developer.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/entity/Developer.java @@ -22,15 +22,8 @@ import com.alibaba.himarket.support.enums.DeveloperAuthType; import com.alibaba.himarket.support.enums.DeveloperStatus; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.*; -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder @Entity @Table( name = "developer", @@ -42,6 +35,11 @@ columnNames = {"portalId", "username"}, name = "uk_portal_username") }) +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor public class Developer extends BaseEntity { @Id diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/entity/DeveloperExternalIdentity.java b/himarket-dal/src/main/java/com/alibaba/himarket/entity/DeveloperExternalIdentity.java index 5581a78b8..e95017660 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/entity/DeveloperExternalIdentity.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/entity/DeveloperExternalIdentity.java @@ -21,15 +21,8 @@ import com.alibaba.himarket.support.enums.DeveloperAuthType; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.*; -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder @Entity @Table( name = "developer_external_identity", @@ -38,6 +31,11 @@ columnNames = {"provider", "subject"}, name = "unique_provider_subject") }) +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor public class DeveloperExternalIdentity extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/entity/Gateway.java b/himarket-dal/src/main/java/com/alibaba/himarket/entity/Gateway.java index c1dbe36c4..f2dd9bd2f 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/entity/Gateway.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/entity/Gateway.java @@ -29,10 +29,8 @@ import com.alibaba.himarket.support.gateway.ApsaraGatewayConfig; import com.alibaba.himarket.support.gateway.HigressConfig; import jakarta.persistence.*; -import lombok.Data; -import lombok.EqualsAndHashCode; +import lombok.*; -@EqualsAndHashCode(callSuper = true) @Entity @Table( name = "gateway", @@ -42,6 +40,10 @@ name = "uk_gateway_id"), }) @Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor public class Gateway extends BaseEntity { @Id diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/entity/NacosInstance.java b/himarket-dal/src/main/java/com/alibaba/himarket/entity/NacosInstance.java index 182c75496..dd978893d 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/entity/NacosInstance.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/entity/NacosInstance.java @@ -20,10 +20,8 @@ package com.alibaba.himarket.entity; import jakarta.persistence.*; -import lombok.Data; -import lombok.EqualsAndHashCode; +import lombok.*; -@EqualsAndHashCode(callSuper = true) @Entity @Table( name = "nacos_instance", @@ -33,6 +31,10 @@ name = "uk_nacos_id"), }) @Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor public class NacosInstance extends BaseEntity { @Id diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/entity/Portal.java b/himarket-dal/src/main/java/com/alibaba/himarket/entity/Portal.java index c43b8409e..eee357520 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/entity/Portal.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/entity/Portal.java @@ -26,10 +26,8 @@ import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; -import lombok.Data; -import lombok.EqualsAndHashCode; +import lombok.*; -@EqualsAndHashCode(callSuper = true) @Entity @Table( name = "portal", @@ -42,6 +40,10 @@ name = "uk_name_admin_id") }) @Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor public class Portal extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -67,5 +69,5 @@ public class Portal extends BaseEntity { @Convert(converter = PortalUiConfigConverter.class) private PortalUiConfig portalUiConfig; - @Transient private List portalDomains = new ArrayList<>(); + @Builder.Default @Transient private List portalDomains = new ArrayList<>(); } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/entity/PortalDomain.java b/himarket-dal/src/main/java/com/alibaba/himarket/entity/PortalDomain.java index cd23a6ec5..050dd00fe 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/entity/PortalDomain.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/entity/PortalDomain.java @@ -22,8 +22,7 @@ import com.alibaba.himarket.support.enums.DomainType; import com.alibaba.himarket.support.enums.ProtocolType; import jakarta.persistence.*; -import lombok.Data; -import lombok.EqualsAndHashCode; +import lombok.*; @Entity @Table( @@ -35,6 +34,9 @@ }) @Data @EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor public class PortalDomain extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -48,9 +50,11 @@ public class PortalDomain extends BaseEntity { @Enumerated(EnumType.STRING) @Column(name = "type", length = 32, nullable = false) + @Builder.Default private DomainType type = DomainType.DEFAULT; @Column(name = "protocol", length = 32, nullable = false) @Enumerated(EnumType.STRING) + @Builder.Default private ProtocolType protocol = ProtocolType.HTTP; } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/entity/Product.java b/himarket-dal/src/main/java/com/alibaba/himarket/entity/Product.java index bb629e183..9945ae8bd 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/entity/Product.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/entity/Product.java @@ -26,10 +26,8 @@ import com.alibaba.himarket.support.product.Icon; import com.alibaba.himarket.support.product.ProductFeature; import jakarta.persistence.*; -import lombok.Data; -import lombok.EqualsAndHashCode; +import lombok.*; -@EqualsAndHashCode(callSuper = true) @Entity @Table( name = "product", @@ -42,6 +40,10 @@ name = "uk_name") }) @Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor public class Product extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -75,6 +77,7 @@ public class Product extends BaseEntity { @Column(name = "status", length = 64) @Enumerated(EnumType.STRING) + @Builder.Default private ProductStatus status = ProductStatus.PENDING; @Column(name = "auto_approve") diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/entity/ProductCategory.java b/himarket-dal/src/main/java/com/alibaba/himarket/entity/ProductCategory.java index d22a1933c..f03ba00a7 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/entity/ProductCategory.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/entity/ProductCategory.java @@ -22,10 +22,8 @@ import com.alibaba.himarket.converter.IconConverter; import com.alibaba.himarket.support.product.Icon; import jakarta.persistence.*; -import lombok.Data; -import lombok.EqualsAndHashCode; +import lombok.*; -@EqualsAndHashCode(callSuper = true) @Entity @Table( name = "product_category", @@ -35,6 +33,10 @@ name = "uk_category_id") }) @Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor public class ProductCategory extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/entity/ProductCategoryRelation.java b/himarket-dal/src/main/java/com/alibaba/himarket/entity/ProductCategoryRelation.java index e8d714687..fb06b0546 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/entity/ProductCategoryRelation.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/entity/ProductCategoryRelation.java @@ -20,10 +20,8 @@ package com.alibaba.himarket.entity; import jakarta.persistence.*; -import lombok.Data; -import lombok.EqualsAndHashCode; +import lombok.*; -@EqualsAndHashCode(callSuper = true) @Entity @Table( name = "product_category_relation", @@ -33,6 +31,10 @@ name = "uk_product_category") }) @Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor public class ProductCategoryRelation extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/entity/ProductPublication.java b/himarket-dal/src/main/java/com/alibaba/himarket/entity/ProductPublication.java index 3424c28c0..a976fb723 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/entity/ProductPublication.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/entity/ProductPublication.java @@ -20,13 +20,15 @@ package com.alibaba.himarket.entity; import jakarta.persistence.*; -import lombok.Data; -import lombok.EqualsAndHashCode; +import lombok.*; -@EqualsAndHashCode(callSuper = true) @Entity @Table(name = "publication") @Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor public class ProductPublication extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/entity/ProductRef.java b/himarket-dal/src/main/java/com/alibaba/himarket/entity/ProductRef.java index 6d2da8b71..128fcae8a 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/entity/ProductRef.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/entity/ProductRef.java @@ -27,13 +27,15 @@ import com.alibaba.himarket.support.product.HigressRefConfig; import com.alibaba.himarket.support.product.NacosRefConfig; import jakarta.persistence.*; -import lombok.Data; -import lombok.EqualsAndHashCode; +import lombok.*; -@EqualsAndHashCode(callSuper = true) @Entity @Table(name = "product_ref") @Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor public class ProductRef extends BaseEntity { @Id diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/entity/ProductSubscription.java b/himarket-dal/src/main/java/com/alibaba/himarket/entity/ProductSubscription.java index 5ab8b1c96..b1f3a6190 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/entity/ProductSubscription.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/entity/ProductSubscription.java @@ -23,8 +23,7 @@ import com.alibaba.himarket.support.consumer.ConsumerAuthConfig; import com.alibaba.himarket.support.enums.SubscriptionStatus; import jakarta.persistence.*; -import lombok.Data; -import lombok.EqualsAndHashCode; +import lombok.*; @Entity @Table( @@ -36,6 +35,9 @@ }) @Data @EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor public class ProductSubscription extends BaseEntity { @Id diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/repository/AdministratorRepository.java b/himarket-dal/src/main/java/com/alibaba/himarket/repository/AdministratorRepository.java index 84977d27e..24ab5fda6 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/repository/AdministratorRepository.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/repository/AdministratorRepository.java @@ -21,10 +21,22 @@ import com.alibaba.himarket.entity.Administrator; import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -public interface AdministratorRepository extends JpaRepository { +public interface AdministratorRepository extends BaseRepository { + + /** + * Find administrator by admin ID + * + * @param adminId the admin ID + * @return the administrator if found + */ Optional findByAdminId(String adminId); + /** + * Find administrator by username + * + * @param username the username + * @return the administrator if found + */ Optional findByUsername(String username); } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ChatAttachmentRepository.java b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ChatAttachmentRepository.java index fc39b1213..bc7de1c02 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ChatAttachmentRepository.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ChatAttachmentRepository.java @@ -21,13 +21,16 @@ import com.alibaba.himarket.entity.ChatAttachment; import java.util.List; -import java.util.Optional; import org.springframework.stereotype.Repository; @Repository public interface ChatAttachmentRepository extends BaseRepository { - Optional findByAttachmentId(String attachmentId); - + /** + * Find attachments by attachment IDs + * + * @param attachmentIds the list of attachment IDs + * @return the list of chat attachments + */ List findByAttachmentIdIn(List attachmentIds); } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ChatRepository.java b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ChatRepository.java index 902f5187c..b0a1f11c7 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ChatRepository.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ChatRepository.java @@ -32,39 +32,41 @@ public interface ChatRepository extends BaseRepository { /** - * Find by sessionId and status + * Find chats by session ID and status * - * @param sessionId - * @param status - * @param sort - * @return + * @param sessionId the session ID + * @param status the chat status + * @param sort the sort order + * @return the list of chats */ List findBySessionIdAndStatus(String sessionId, ChatStatus status, Sort sort); /** - * Find by chatId + * Find chat by chat ID * - * @param chatId - * @return + * @param chatId the chat ID + * @return the chat if found */ Optional findByChatId(String chatId); /** - * Find all chats for given sessionId and userId + * Find all chats by session ID and user ID * - * @param sessionId - * @param userId - * @param sort - * @return + * @param sessionId the session ID + * @param userId the user ID + * @param sort the sort order + * @return the list of chats */ List findAllBySessionIdAndUserId(String sessionId, String userId, Sort sort); /** - * Find next sequence for given conversationId and questionId + * Find current sequence number for a conversation * - * @param conversationId - * @param questionId - * @return + * @param sessionId the session ID + * @param conversationId the conversation ID + * @param questionId the question ID + * @param productId the product ID + * @return the current sequence number */ @Query( "SELECT COALESCE(MAX(c.sequence), 0) " @@ -80,9 +82,9 @@ Integer findCurrentSequence( @Param("productId") String productId); /** - * Delete all chats for given sessionId + * Delete all chats by session ID * - * @param sessionId + * @param sessionId the session ID */ void deleteAllBySessionId(String sessionId); } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ChatSessionRepository.java b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ChatSessionRepository.java index 2bf661f90..ea9fd567c 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ChatSessionRepository.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ChatSessionRepository.java @@ -20,7 +20,6 @@ package com.alibaba.himarket.repository; import com.alibaba.himarket.entity.ChatSession; -import java.util.List; import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -31,32 +30,45 @@ public interface ChatSessionRepository extends BaseRepository { /** - * Find a session by session ID + * Find session by session ID + * + * @param sessionId the session ID + * @return the chat session if found */ Optional findBySessionId(String sessionId); /** - * Find sessions by user ID + * Find sessions by user ID with pagination + * + * @param userId the user ID + * @param pageable the pagination information + * @return the page of chat sessions */ Page findByUserId(String userId, Pageable pageable); /** - * Calculate the number of sessions for a user + * Count sessions by user ID + * + * @param userId the user ID + * @return the number of sessions */ int countByUserId(String userId); /** - * Find a session by session ID and user ID + * Find session by session ID and user ID + * + * @param sessionId the session ID + * @param userId the user ID + * @return the chat session if found */ Optional findBySessionIdAndUserId(String sessionId, String userId); /** - * Find all sessions for a user - */ - List findAllByUserId(String userId); - - /** - * Find the first session for a user + * Find first session by user ID + * + * @param userId the user ID + * @param sort the sort order + * @return the first chat session if found */ Optional findFirstByUserId(String userId, Sort sort); } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ConsumerCredentialRepository.java b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ConsumerCredentialRepository.java index 5cb8c68e4..c6e60e03a 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ConsumerCredentialRepository.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ConsumerCredentialRepository.java @@ -24,7 +24,18 @@ public interface ConsumerCredentialRepository extends BaseRepository { + /** + * Find credential by consumer ID + * + * @param consumerId the consumer ID + * @return the consumer credential if found + */ Optional findByConsumerId(String consumerId); + /** + * Delete all credentials by consumer ID + * + * @param consumerId the consumer ID + */ void deleteAllByConsumerId(String consumerId); } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ConsumerRefRepository.java b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ConsumerRefRepository.java index c09c4110e..9e7b07337 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ConsumerRefRepository.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ConsumerRefRepository.java @@ -21,31 +21,26 @@ import com.alibaba.himarket.entity.ConsumerRef; import com.alibaba.himarket.support.enums.GatewayType; -import com.alibaba.himarket.support.gateway.GatewayConfig; import java.util.List; -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository -public interface ConsumerRefRepository - extends JpaRepository, JpaSpecificationExecutor { +public interface ConsumerRefRepository extends BaseRepository { + /** + * Find all consumer references by consumer ID + * + * @param consumerId the consumer ID + * @return the list of consumer references + */ List findAllByConsumerId(String consumerId); - @Query( - "SELECT c FROM ConsumerRef c WHERE c.consumerId = :consumerId AND c.gatewayType =" - + " :gatewayType AND c.gatewayConfig = :gatewayConfig") - @Deprecated - Optional findConsumerRef( - @Param("consumerId") String consumerId, - @Param("gatewayType") GatewayType gatewayType, - @Param("gatewayConfig") GatewayConfig gatewayConfig); - - Optional findByGwConsumerId(String gwConsumerId); - + /** + * Find consumer references by consumer ID and gateway type + * + * @param consumerId the consumer ID + * @param gatewayType the gateway type + * @return the list of consumer references + */ List findAllByConsumerIdAndGatewayType(String consumerId, GatewayType gatewayType); } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ConsumerRepository.java b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ConsumerRepository.java index 099a6e376..964899f6d 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ConsumerRepository.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ConsumerRepository.java @@ -23,8 +23,6 @@ import java.util.Collection; import java.util.List; import java.util.Optional; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -32,30 +30,61 @@ public interface ConsumerRepository extends BaseRepository { + /** + * Find consumer by consumer ID + * + * @param consumerId the consumer ID + * @return the consumer if found + */ Optional findByConsumerId(String consumerId); + /** + * Find consumer by developer ID and consumer ID + * + * @param developerId the developer ID + * @param consumerId the consumer ID + * @return the consumer if found + */ Optional findByDeveloperIdAndConsumerId(String developerId, String consumerId); - Optional findByDeveloperIdAndName(String developerId, String name); - - Page findByDeveloperId(String developerId, Pageable pageable); - - Page findByPortalId(String portalId, Pageable pageable); - + /** + * Find all consumers by developer ID + * + * @param developerId the developer ID + * @return the list of consumers + */ List findAllByDeveloperId(String developerId); - void deleteAllByDeveloperId(String developerId); - + /** + * Find consumers by consumer IDs + * + * @param consumerIds the collection of consumer IDs + * @return the list of consumers + */ List findByConsumerIdIn(Collection consumerIds); + /** + * Find first consumer by developer ID + * + * @param developerId the developer ID + * @param sort the sort order + * @return the first consumer if found + */ Optional findFirstByDeveloperId(String developerId, Sort sort); + /** + * Find primary consumer by developer ID + * + * @param developerId the developer ID + * @param isPrimary the primary flag + * @return the primary consumer if found + */ Optional findByDeveloperIdAndIsPrimary(String developerId, Boolean isPrimary); /** * Clear primary flag for all consumers under a developer * - * @param developerId + * @param developerId the developer ID */ @Modifying @Query("UPDATE Consumer c SET c.isPrimary = NULL WHERE c.developerId = :developerId") diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/repository/DeveloperExternalIdentityRepository.java b/himarket-dal/src/main/java/com/alibaba/himarket/repository/DeveloperExternalIdentityRepository.java index 97c54058e..447ae7ba1 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/repository/DeveloperExternalIdentityRepository.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/repository/DeveloperExternalIdentityRepository.java @@ -20,19 +20,24 @@ package com.alibaba.himarket.repository; import com.alibaba.himarket.entity.DeveloperExternalIdentity; -import java.util.List; import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; public interface DeveloperExternalIdentityRepository - extends JpaRepository { - - List findByDeveloper_DeveloperId(String developerId); + extends BaseRepository { + /** + * Find external identity by provider and subject + * + * @param provider the provider name + * @param subject the subject identifier + * @return the developer external identity if found + */ Optional findByProviderAndSubject(String provider, String subject); - void deleteByProviderAndSubjectAndDeveloper_DeveloperId( - String provider, String subject, String developerId); - + /** + * Delete external identities by developer ID + * + * @param developerId the developer ID + */ void deleteByDeveloper_DeveloperId(String developerId); } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/repository/DeveloperRepository.java b/himarket-dal/src/main/java/com/alibaba/himarket/repository/DeveloperRepository.java index 443c87ec8..8a5da4069 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/repository/DeveloperRepository.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/repository/DeveloperRepository.java @@ -22,22 +22,31 @@ import com.alibaba.himarket.entity.Developer; import java.util.List; import java.util.Optional; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; public interface DeveloperRepository extends BaseRepository { + /** + * Find developer by developer ID + * + * @param developerId the developer ID + * @return the developer if found + */ Optional findByDeveloperId(String developerId); - Optional findByUsername(String username); - + /** + * Find all developers by portal ID + * + * @param portalId the portal ID + * @return the list of developers + */ List findByPortalId(String portalId); + /** + * Find developer by portal ID and username + * + * @param portalId the portal ID + * @param username the username + * @return the developer if found + */ Optional findByPortalIdAndUsername(String portalId, String username); - - Optional findByPortalIdAndEmail(String portalId, String email); - - Optional findByDeveloperIdAndPortalId(String developerId, String portalId); - - Page findByPortalId(String portalId, Pageable pageable); } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/repository/GatewayRepository.java b/himarket-dal/src/main/java/com/alibaba/himarket/repository/GatewayRepository.java index d2dcb4f48..17e90d5e2 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/repository/GatewayRepository.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/repository/GatewayRepository.java @@ -21,14 +21,22 @@ import com.alibaba.himarket.entity.Gateway; import java.util.Optional; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; public interface GatewayRepository extends BaseRepository { + /** + * Find gateway by gateway ID + * + * @param gatewayId the gateway ID + * @return the gateway if found + */ Optional findByGatewayId(String gatewayId); + /** + * Find gateway by gateway name + * + * @param gatewayName the gateway name + * @return the gateway if found + */ Optional findByGatewayName(String gatewayName); - - Page findByAdminId(String adminId, Pageable pageable); } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/repository/NacosInstanceRepository.java b/himarket-dal/src/main/java/com/alibaba/himarket/repository/NacosInstanceRepository.java index c1a62d57d..46a1ecc3e 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/repository/NacosInstanceRepository.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/repository/NacosInstanceRepository.java @@ -26,7 +26,19 @@ @Repository public interface NacosInstanceRepository extends BaseRepository { + /** + * Find Nacos instance by Nacos ID + * + * @param nacosId the Nacos ID + * @return the Nacos instance if found + */ Optional findByNacosId(String nacosId); + /** + * Find Nacos instance by Nacos name + * + * @param nacosName the Nacos name + * @return the Nacos instance if found + */ Optional findByNacosName(String nacosName); } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/repository/PortalDomainRepository.java b/himarket-dal/src/main/java/com/alibaba/himarket/repository/PortalDomainRepository.java index 9478301f1..60e3bdb26 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/repository/PortalDomainRepository.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/repository/PortalDomainRepository.java @@ -25,13 +25,43 @@ public interface PortalDomainRepository extends BaseRepository { + /** + * Find portal domain by domain + * + * @param domain the domain name + * @return the portal domain if found + */ Optional findByDomain(String domain); + /** + * Find portal domain by portal ID and domain + * + * @param portalId the portal ID + * @param domain the domain name + * @return the portal domain if found + */ Optional findByPortalIdAndDomain(String portalId, String domain); + /** + * Find all portal domains by portal ID + * + * @param portalId the portal ID + * @return the list of portal domains + */ List findAllByPortalId(String portalId); + /** + * Find all portal domains by portal IDs + * + * @param portalIds the list of portal IDs + * @return the list of portal domains + */ List findAllByPortalIdIn(List portalIds); + /** + * Delete all portal domains by portal ID + * + * @param portalId the portal ID + */ void deleteAllByPortalId(String portalId); } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/repository/PortalRepository.java b/himarket-dal/src/main/java/com/alibaba/himarket/repository/PortalRepository.java index 4e1f08dcb..bb65e07b7 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/repository/PortalRepository.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/repository/PortalRepository.java @@ -21,20 +21,29 @@ import com.alibaba.himarket.entity.Portal; import java.util.Optional; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; public interface PortalRepository extends BaseRepository { + /** + * Find first portal ordered by ID ascending + * + * @return the first portal if found + */ Optional findFirstByOrderByIdAsc(); - Optional findByPortalIdAndAdminId(String portalId, String adminId); - + /** + * Find portal by portal ID + * + * @param portalId the portal ID + * @return the portal if found + */ Optional findByPortalId(String portalId); - Optional findByNameAndAdminId(String name, String adminId); - + /** + * Find portal by name + * + * @param name the portal name + * @return the portal if found + */ Optional findByName(String name); - - Page findByAdminId(String adminId, Pageable pageable); } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ProductCategoryRelationRepository.java b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ProductCategoryRelationRepository.java index f99ee4d5c..a59223a27 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ProductCategoryRelationRepository.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ProductCategoryRelationRepository.java @@ -28,19 +28,46 @@ public interface ProductCategoryRelationRepository extends BaseRepository { + /** + * Find product-category relations by product ID + * + * @param productId the product ID + * @return the list of product-category relations + */ List findByProductId(String productId); + /** + * Find product-category relations by category ID + * + * @param categoryId the category ID + * @return the list of product-category relations + */ List findByCategoryId(String categoryId); + /** + * Check if category ID exists in relations + * + * @param categoryId the category ID + * @return true if exists, false otherwise + */ boolean existsByCategoryId(String categoryId); + /** + * Delete all product-category relations by product ID + * + * @param productId the product ID + */ @Modifying @Transactional void deleteAllByProductId(String productId); + /** + * Delete product-category relations by product IDs and category ID + * + * @param productIds the collection of product IDs + * @param categoryId the category ID + */ @Modifying @Transactional void deleteByProductIdInAndCategoryId(Collection productIds, String categoryId); - - List findByCategoryIdIn(Collection categoryId); } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ProductCategoryRepository.java b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ProductCategoryRepository.java index 0fa37d4a9..7400e9ca7 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ProductCategoryRepository.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ProductCategoryRepository.java @@ -27,9 +27,27 @@ @Repository public interface ProductCategoryRepository extends BaseRepository { + /** + * Find product category by category ID + * + * @param categoryId the category ID + * @return the product category if found + */ Optional findByCategoryId(String categoryId); + /** + * Find product category by name + * + * @param name the category name + * @return the product category if found + */ Optional findByName(String name); + /** + * Find product categories by category IDs + * + * @param categoryIds the list of category IDs + * @return the list of product categories + */ List findByCategoryIdIn(List categoryIds); } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ProductPublicationRepository.java b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ProductPublicationRepository.java index ede8782e7..2f917b701 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ProductPublicationRepository.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ProductPublicationRepository.java @@ -26,19 +26,69 @@ public interface ProductPublicationRepository extends BaseRepository { + /** + * Find product publication by publication ID + * + * @param publicationId the publication ID + * @return the product publication if found + */ Optional findByPublicationId(String publicationId); + /** + * Find product publications by portal ID with pagination + * + * @param portalId the portal ID + * @param pageable the pagination information + * @return the page of product publications + */ Page findByPortalId(String portalId, Pageable pageable); + /** + * Find product publication by portal ID and product ID + * + * @param portalId the portal ID + * @param productId the product ID + * @return the product publication if found + */ Optional findByPortalIdAndProductId(String portalId, String productId); + /** + * Find product publications by product ID with pagination + * + * @param productId the product ID + * @param pageable the pagination information + * @return the page of product publications + */ Page findByProductId(String productId, Pageable pageable); + /** + * Delete product publication by product ID + * + * @param productId the product ID + */ void deleteByProductId(String productId); + /** + * Delete all product publications by portal ID + * + * @param portalId the portal ID + */ void deleteAllByPortalId(String portalId); + /** + * Check if product publication exists by product ID + * + * @param productId the product ID + * @return true if exists, false otherwise + */ boolean existsByProductId(String productId); + /** + * Check if product is published to portals other than specified portal ID + * + * @param productId the product ID + * @param portalId the portal ID to exclude + * @return true if published to other portals, false otherwise + */ boolean existsByProductIdAndPortalIdNot(String productId, String portalId); } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ProductRefRepository.java b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ProductRefRepository.java index d36e0b978..c80e398c2 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ProductRefRepository.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ProductRefRepository.java @@ -21,19 +21,39 @@ import com.alibaba.himarket.entity.ProductRef; import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.stereotype.Repository; @Repository -public interface ProductRefRepository - extends JpaRepository, JpaSpecificationExecutor { +public interface ProductRefRepository extends BaseRepository { + /** + * Find product reference by product ID + * + * @param productId the product ID + * @return the product reference if found + */ Optional findByProductId(String productId); + /** + * Find first product reference by product ID + * + * @param productId the product ID + * @return the first product reference if found + */ Optional findFirstByProductId(String productId); + /** + * Check if gateway ID exists in product references + * + * @param gatewayId the gateway ID + * @return true if exists, false otherwise + */ boolean existsByGatewayId(String gatewayId); + /** + * Delete product reference by product ID + * + * @param productId the product ID + */ void deleteByProductId(String productId); } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ProductRepository.java b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ProductRepository.java index ed0c326df..867e1f0c5 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/repository/ProductRepository.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/repository/ProductRepository.java @@ -23,22 +23,33 @@ import java.util.Collection; import java.util.List; import java.util.Optional; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; @Repository public interface ProductRepository extends BaseRepository { + /** + * Find product by product ID + * + * @param productId the product ID + * @return the product if found + */ Optional findByProductId(String productId); - Optional findByProductIdAndAdminId(String productId, String adminId); - + /** + * Find product by name and admin ID + * + * @param name the product name + * @param adminId the admin ID + * @return the product if found + */ Optional findByNameAndAdminId(String name, String adminId); - Page findByProductIdIn(Collection productIds, Pageable pageable); - + /** + * Find products by product IDs + * + * @param productIds the collection of product IDs + * @return the list of products + */ List findByProductIdIn(Collection productIds); - - Page findByAdminId(String adminId, Pageable pageable); } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/repository/SubscriptionRepository.java b/himarket-dal/src/main/java/com/alibaba/himarket/repository/SubscriptionRepository.java index 678d74152..b8f70d00f 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/repository/SubscriptionRepository.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/repository/SubscriptionRepository.java @@ -25,17 +25,58 @@ public interface SubscriptionRepository extends BaseRepository { + /** + * Find subscription by subscription ID + * + * @param subscriptionId the subscription ID + * @return the product subscription if found + */ Optional findBySubscriptionId(String subscriptionId); + /** + * Find subscription by consumer ID and product ID + * + * @param consumerId the consumer ID + * @param productId the product ID + * @return the product subscription if found + */ Optional findByConsumerIdAndProductId(String consumerId, String productId); + /** + * Find all subscriptions by consumer ID + * + * @param consumerId the consumer ID + * @return the list of product subscriptions + */ List findAllByConsumerId(String consumerId); + /** + * Find all subscriptions by product ID + * + * @param productId the product ID + * @return the list of product subscriptions + */ List findAllByProductId(String productId); + /** + * Delete all subscriptions by consumer ID + * + * @param consumerId the consumer ID + */ void deleteAllByConsumerId(String consumerId); + /** + * Delete all subscriptions by product ID + * + * @param productId the product ID + */ void deleteAllByProductId(String productId); + /** + * Delete subscription by consumer ID and product ID + * + * @param consumerId the consumer ID + * @param productId the product ID + */ void deleteByConsumerIdAndProductId(String consumerId, String productId); } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/support/chat/ChatUsage.java b/himarket-dal/src/main/java/com/alibaba/himarket/support/chat/ChatUsage.java index 62e0304ba..6dadf6509 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/support/chat/ChatUsage.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/support/chat/ChatUsage.java @@ -19,8 +19,6 @@ package com.alibaba.himarket.support.chat; -import cn.hutool.core.annotation.Alias; -import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; import lombok.Data; @@ -28,35 +26,13 @@ @Builder public class ChatUsage { - @JsonProperty("elapsed_time") - @Alias("elapsed_time") private Long elapsedTime; - @JsonProperty("first_byte_timeout") - @Alias("first_byte_timeout") private Long firstByteTimeout; - @JsonProperty("prompt_tokens") - @Alias("prompt_tokens") - private Integer promptTokens; + private Integer inputTokens; - @JsonProperty("completion_tokens") - @Alias("completion_tokens") - private Integer completionTokens; + private Integer outputTokens; - @JsonProperty("total_tokens") - @Alias("total_tokens") private Integer totalTokens; - - @JsonProperty("prompt_tokens_details") - @Alias("prompt_tokens_details") - private PromptTokensDetails promptTokensDetails; - - @Data - @Builder - public static class PromptTokensDetails { - @JsonProperty("cached_tokens") - @Alias("cached_tokens") - private Integer cachedTokens; - } } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/support/chat/mcp/MCPTransportConfig.java b/himarket-dal/src/main/java/com/alibaba/himarket/support/chat/mcp/MCPTransportConfig.java index ea0e549ed..185db793c 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/support/chat/mcp/MCPTransportConfig.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/support/chat/mcp/MCPTransportConfig.java @@ -20,6 +20,7 @@ package com.alibaba.himarket.support.chat.mcp; import com.alibaba.himarket.support.enums.MCPTransportMode; +import java.util.Map; import lombok.Builder; import lombok.Data; @@ -32,4 +33,8 @@ public class MCPTransportConfig { private MCPTransportMode transportMode; private String url; + + private Map headers; + + private Map queryParams; } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/support/enums/AIProtocol.java b/himarket-dal/src/main/java/com/alibaba/himarket/support/enums/AIProtocol.java index 42cde8203..67e5c1540 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/support/enums/AIProtocol.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/support/enums/AIProtocol.java @@ -19,9 +19,22 @@ package com.alibaba.himarket.support.enums; +import lombok.Getter; + +@Getter public enum AIProtocol { - OPENAI, + OPENAI("OpenAI/V1"), + + ANTHROPIC("Anthropic"), + + DASHSCOPE("DashScope"), - ANTHROPIC, + DASHSCOPE_IMAGE("DashScopeImage"), ; + + private final String protocol; + + AIProtocol(String protocol) { + this.protocol = protocol; + } } diff --git a/himarket-dal/src/main/java/com/alibaba/himarket/support/product/ModelFeature.java b/himarket-dal/src/main/java/com/alibaba/himarket/support/product/ModelFeature.java index 1270642fe..c11f77722 100644 --- a/himarket-dal/src/main/java/com/alibaba/himarket/support/product/ModelFeature.java +++ b/himarket-dal/src/main/java/com/alibaba/himarket/support/product/ModelFeature.java @@ -49,4 +49,9 @@ public class ModelFeature { * Enable web search */ private Boolean webSearch; + + /** + * Enable thinking + */ + private Boolean enableThinking; } diff --git a/himarket-server/lib/csb220230206-1.5.3-cleaned.jar b/himarket-server/lib/csb220230206-1.5.3-cleaned.jar new file mode 100644 index 000000000..ff9d18beb Binary files /dev/null and b/himarket-server/lib/csb220230206-1.5.3-cleaned.jar differ diff --git a/himarket-server/pom.xml b/himarket-server/pom.xml index 9e1f9b398..8ad34b563 100644 --- a/himarket-server/pom.xml +++ b/himarket-server/pom.xml @@ -55,11 +55,6 @@ spring-boot-starter-security - - - - - org.springframework.boot spring-boot-starter-thymeleaf @@ -129,6 +124,14 @@ commons-io commons-io + + fastjson + com.alibaba + + + error_prone_annotations + com.google.errorprone + @@ -150,6 +153,12 @@ com.github.rholder guava-retrying + + + jsr305 + com.google.code.findbugs + + @@ -164,7 +173,7 @@ csb220230206 1.5.3-SNAPSHOT system - ${project.basedir}/lib/csb220230206-1.5.3.jar + ${project.basedir}/lib/csb220230206-1.5.3-cleaned.jar @@ -175,16 +184,46 @@ org.springframework.ai spring-ai-starter-model-openai + + + mcp-json-jackson2 + io.modelcontextprotocol.sdk + + + antlr4-runtime + org.antlr + + org.springframework.ai spring-ai-starter-mcp-client + + + swagger-annotations-jakarta + io.swagger.core.v3 + + + mcp + io.modelcontextprotocol.sdk + + com.aliyun.openservices aliyun-log + + + httpclient + org.apache.httpcomponents + + + protobuf-java + com.google.protobuf + + @@ -204,6 +243,43 @@ true + + io.agentscope + agentscope + + + dashscope-sdk-java + com.alibaba + + + + + + com.openai + openai-java + + + error_prone_annotations + com.google.errorprone + + + + + + com.alibaba + dashscope-sdk-java + + + jsonschema-generator + com.github.victools + + + slf4j-simple + org.slf4j + + + + \ No newline at end of file diff --git a/himarket-server/src/main/java/com/alibaba/himarket/controller/ChatController.java b/himarket-server/src/main/java/com/alibaba/himarket/controller/ChatController.java index ddce0f26b..269a618a3 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/controller/ChatController.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/controller/ChatController.java @@ -22,7 +22,8 @@ import com.alibaba.himarket.core.annotation.AdminOrDeveloperAuth; import com.alibaba.himarket.dto.params.chat.CreateChatParam; import com.alibaba.himarket.dto.result.chat.ChatAnswerMessage; -import com.alibaba.himarket.service.ChatService; +import com.alibaba.himarket.service.hichat.service.ChatService; +import com.alibaba.himarket.service.legacy.ChatServiceLegacy; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -33,7 +34,9 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import reactor.core.publisher.Flux; +import reactor.core.scheduler.Schedulers; @RestController @RequestMapping("/chats") @@ -43,11 +46,36 @@ @AdminOrDeveloperAuth public class ChatController { + private final ChatServiceLegacy chatServiceLegacy; + private final ChatService chatService; - @PostMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public Flux chat( + @PostMapping(value = "/legacy", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public Flux chatLegacy( @Valid @RequestBody CreateChatParam param, HttpServletResponse response) { - return chatService.chat(param, response); + return chatServiceLegacy.chat(param, response); + } + + @PostMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter chat(@Valid @RequestBody CreateChatParam param) { + // Use SseEmitter for streaming + SseEmitter emitter = new SseEmitter(30 * 60 * 1000L); + + chatService + .chat(param) + .subscribeOn(Schedulers.boundedElastic()) + .subscribe( + event -> { + try { + emitter.send(event); + } catch (Exception e) { + log.error("Failed to send event", e); + emitter.completeWithError(e); + } + }, + emitter::completeWithError, + emitter::complete); + + return emitter; } } diff --git a/himarket-server/src/main/java/com/alibaba/himarket/core/advice/ResponseAdvice.java b/himarket-server/src/main/java/com/alibaba/himarket/core/advice/ResponseAdvice.java index 8549db75d..94c4e18f8 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/core/advice/ResponseAdvice.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/core/advice/ResponseAdvice.java @@ -33,6 +33,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import reactor.core.publisher.Flux; /** * Unified response wrapper @@ -64,7 +65,8 @@ public boolean supports( Class type = returnType.getMethod().getReturnType(); return !type.equals(ResponseEntity.class) && !type.equals(Response.class) - && !type.equals(SseEmitter.class); + && !type.equals(SseEmitter.class) + && !type.equals(Flux.class); } @Override diff --git a/himarket-server/src/main/java/com/alibaba/himarket/core/constant/JwtConstants.java b/himarket-server/src/main/java/com/alibaba/himarket/core/constant/JwtConstants.java index 4b7131942..314865e69 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/core/constant/JwtConstants.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/core/constant/JwtConstants.java @@ -23,13 +23,19 @@ public class JwtConstants { // region JWT Header - /** Algorithm */ + /** + * Algorithm + */ public static final String HEADER_ALG = "alg"; - /** Type */ + /** + * Type + */ public static final String HEADER_TYP = "typ"; - /** Key ID */ + /** + * Key ID + */ public static final String HEADER_KID = "kid"; // endregion @@ -38,55 +44,83 @@ public class JwtConstants { public static final String PAYLOAD_PROVIDER = "provider"; - /** Expiration */ + /** + * Expiration + */ public static final String PAYLOAD_EXP = "exp"; - /** Issued at */ + /** + * Issued at + */ public static final String PAYLOAD_IAT = "iat"; - /** JWT ID */ + /** + * JWT ID + */ public static final String PAYLOAD_JTI = "jti"; - /** Issuer */ + /** + * Issuer + */ public static final String PAYLOAD_ISS = "iss"; - /** Subject */ + /** + * Subject + */ public static final String PAYLOAD_SUB = "sub"; - /** Audience */ + /** + * Audience + */ public static final String PAYLOAD_AUD = "aud"; - /** Portal ID */ + /** + * Portal ID + */ public static final String PAYLOAD_PORTAL = "portal"; // endregion // region Custom Payload - /** User ID (default identity mapping) */ + /** + * User ID (default identity mapping) + */ public static final String PAYLOAD_USER_ID = "userId"; - /** User name (default identity mapping) */ + /** + * User name (default identity mapping) + */ public static final String PAYLOAD_USER_NAME = "name"; - /** Email (default identity mapping) */ + /** + * Email (default identity mapping) + */ public static final String PAYLOAD_EMAIL = "email"; // endregion // region OAuth2 Constants - /** JWT Bearer grant type */ + /** + * JWT Bearer grant type + */ public static final String JWT_BEARER_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"; - /** Token type */ + /** + * Token type + */ public static final String TOKEN_TYPE_BEARER = "Bearer"; - /** Default token expiration (seconds) */ + /** + * Default token expiration (seconds) + */ public static final int DEFAULT_TOKEN_EXPIRES_IN = 3600; - /** JWT token type */ + /** + * JWT token type + */ public static final String JWT_TOKEN_TYPE = "JWT"; // endregion diff --git a/himarket-server/src/main/java/com/alibaba/himarket/core/event/McpClientRemovedEvent.java b/himarket-server/src/main/java/com/alibaba/himarket/core/event/McpClientRemovedEvent.java new file mode 100644 index 000000000..e4e1e3bf2 --- /dev/null +++ b/himarket-server/src/main/java/com/alibaba/himarket/core/event/McpClientRemovedEvent.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.alibaba.himarket.core.event; + +import com.alibaba.himarket.service.hichat.manager.ToolManager; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class McpClientRemovedEvent { + + /** + * The cache key of the removed MCP client, see {@link ToolManager} + */ + private final String mcpCacheKey; +} diff --git a/himarket-server/src/main/java/com/alibaba/himarket/core/exception/ChatError.java b/himarket-server/src/main/java/com/alibaba/himarket/core/exception/ChatError.java index 0af262ce1..926196d27 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/core/exception/ChatError.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/core/exception/ChatError.java @@ -19,19 +19,34 @@ package com.alibaba.himarket.core.exception; +import io.agentscope.core.model.ModelException; +import java.io.IOException; import java.util.concurrent.TimeoutException; import lombok.Getter; import org.springframework.web.reactive.function.client.WebClientResponseException; @Getter public enum ChatError { - WEB_RESPONSE_ERROR("Web response error"), - - TIMEOUT("Timeout"), + /** + * Unknown error + */ UNKNOWN_ERROR("Unknown error"), - LLM_ERROR("LLM error, please check the input"), + /** + * Network connection error + */ + NETWORK_ERROR("Network connection error"), + + /** + * Request timeout + */ + TIMEOUT("Request timeout"), + + /** + * Request failed + */ + REQUEST_ERROR("Request failed"), ; private final String description; @@ -40,13 +55,27 @@ public enum ChatError { this.description = description; } + /** + * Determine ChatError type from exception. + * + * @param error The throwable to classify + * @return Corresponding ChatError enum value + */ public static ChatError from(Throwable error) { - if (error instanceof WebClientResponseException) { - return WEB_RESPONSE_ERROR; - } if (error instanceof TimeoutException) { return TIMEOUT; } + + // Network connection errors (network unreachable, connection refused, etc.) + if (error instanceof IOException) { + return NETWORK_ERROR; + } + + // HTTP errors from model (401, 403, 500, etc.) + if (error instanceof WebClientResponseException || error instanceof ModelException) { + return REQUEST_ERROR; + } + return UNKNOWN_ERROR; } } diff --git a/himarket-server/src/main/java/com/alibaba/himarket/core/utils/CacheUtil.java b/himarket-server/src/main/java/com/alibaba/himarket/core/utils/CacheUtil.java index bb405bab8..df690bcc9 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/core/utils/CacheUtil.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/core/utils/CacheUtil.java @@ -21,7 +21,10 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.RemovalCause; +import com.github.benmanes.caffeine.cache.RemovalListener; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; public class CacheUtil { @@ -40,4 +43,64 @@ public static Cache newCache(long expireAfterWrite) { .expireAfterWrite(expireAfterWrite, TimeUnit.MINUTES) .build(); } + + /** + * Caffeine cache with expire callback + * + * @param expireAfterWrite + * @param onExpire + * @param + * @param + * @return + */ + public static Cache newCache(long expireAfterWrite, Consumer onExpire) { + return Caffeine.newBuilder() + .initialCapacity(10) + .maximumSize(10000) + .expireAfterWrite(expireAfterWrite, TimeUnit.MINUTES) + .removalListener( + (K key, V value, RemovalCause cause) -> { + if (cause.equals(RemovalCause.EXPIRED) + && value != null + && onExpire != null) { + onExpire.accept(value); + } + }) + .build(); + } + + /** + * Create LRU cache with time-based eviction + * + * @param expireAfterAccess expire after N seconds of no access + * @param key type + * @param value type + * @return Cache instance + */ + public static Cache newLRUCache(long expireAfterAccess) { + return Caffeine.newBuilder() + .initialCapacity(10) + .maximumSize(10000) + .expireAfterAccess(expireAfterAccess, TimeUnit.SECONDS) + .build(); + } + + /** + * Create LRU cache with time-based eviction and removal listener + * + * @param expireAfterAccess expire after N seconds of no access + * @param removalListener listener to be invoked when an entry is removed + * @param key type + * @param value type + * @return Cache instance + */ + public static Cache newLRUCache( + long expireAfterAccess, RemovalListener removalListener) { + return Caffeine.newBuilder() + .initialCapacity(10) + .maximumSize(10000) + .expireAfterAccess(expireAfterAccess, TimeUnit.SECONDS) + .removalListener(removalListener) + .build(); + } } diff --git a/himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/CreateChatParam.java b/himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/CreateChatParam.java index 74ab50d44..57a09a67d 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/CreateChatParam.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/CreateChatParam.java @@ -29,39 +29,59 @@ @Data public class CreateChatParam implements InputConverter { - /** Session ID */ - @NotBlank(message = "sessionId cannot be empty") + /** + * Session ID + */ + @NotBlank(message = "SessionId cannot be empty") private String sessionId; - /** Conversation ID */ - @NotBlank(message = "conversationId cannot be empty") + /** + * Conversation ID + */ + @NotBlank(message = "ConversationId cannot be empty") private String conversationId; - /** Question ID, generated by the client */ - @NotBlank(message = "questionId cannot be empty") + /** + * Question ID, generated by the client + */ + @NotBlank(message = "QuestionId cannot be empty") private String questionId; - /** Answer ID, generated by server */ + /** + * Answer ID, generated by server + */ private String answerId; - /** Product ID, must be the same as the product ID used to create the session */ - @NotBlank(message = "productId cannot be empty") + /** + * Product ID, must be the same as the product ID used to create the session + */ + @NotBlank(message = "ProductId cannot be empty") private String productId; - /** Question */ - @NotBlank(message = "question cannot be empty") + /** + * Question + */ + @NotBlank(message = "Question cannot be empty") private String question; - /** Multi-modal content */ + /** + * Multi-modal content + */ private List attachments; - /** MCP servers to use in chat */ + /** + * MCP servers to use in chat + */ private List mcpProducts; - /** If need stream */ + /** + * If need stream + */ private Boolean stream = true; - /** If need memory */ + /** + * If need memory + */ private Boolean needMemory = true; private Boolean enableWebSearch = false; diff --git a/himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/CreateChatSessionParam.java b/himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/CreateChatSessionParam.java index bd370464f..be561a94a 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/CreateChatSessionParam.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/CreateChatSessionParam.java @@ -31,15 +31,21 @@ @Data public class CreateChatSessionParam implements InputConverter { - /** Products to use */ + /** + * Products to use + */ @NotEmpty(message = "products cannot be empty") private List products; - /** Model or Agent */ + /** + * Model or Agent + */ @NotNull(message = "talkType cannot be null") private TalkType talkType; - /** Session name */ + /** + * Session name + */ @NotBlank(message = "name cannot be empty") private String name; } diff --git a/himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/McpToolMeta.java b/himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/McpToolMeta.java index 11bf04375..8df51365c 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/McpToolMeta.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/McpToolMeta.java @@ -26,6 +26,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor +@Deprecated public class McpToolMeta { /** 工具名称 */ private String toolName; diff --git a/himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/ToolContext.java b/himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/ToolContext.java index 1be8ba46f..5d61860b8 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/ToolContext.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/ToolContext.java @@ -30,6 +30,7 @@ @Getter @NoArgsConstructor +@Deprecated public class ToolContext { private final List toolCallbacks = new ArrayList<>(); private final Map toolDefinitionMap = new HashMap<>(); diff --git a/himarket-server/src/main/java/com/alibaba/himarket/dto/params/nacos/QueryNacosNamespaceParam.java b/himarket-server/src/main/java/com/alibaba/himarket/dto/params/nacos/QueryNacosNamespaceParam.java index d741da680..8f506810d 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/dto/params/nacos/QueryNacosNamespaceParam.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/dto/params/nacos/QueryNacosNamespaceParam.java @@ -20,7 +20,9 @@ package com.alibaba.himarket.dto.params.nacos; import lombok.Data; +import lombok.EqualsAndHashCode; +@EqualsAndHashCode(callSuper = true) @Data public class QueryNacosNamespaceParam extends CreateNacosParam { private String namespace; diff --git a/himarket-server/src/main/java/com/alibaba/himarket/dto/result/chat/LlmChatRequest.java b/himarket-server/src/main/java/com/alibaba/himarket/dto/result/chat/LlmChatRequestLegacy.java similarity index 98% rename from himarket-server/src/main/java/com/alibaba/himarket/dto/result/chat/LlmChatRequest.java rename to himarket-server/src/main/java/com/alibaba/himarket/dto/result/chat/LlmChatRequestLegacy.java index accfde4d5..8c617acc8 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/dto/result/chat/LlmChatRequest.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/dto/result/chat/LlmChatRequestLegacy.java @@ -37,7 +37,7 @@ @Data @Builder @Slf4j -public class LlmChatRequest { +public class LlmChatRequestLegacy { /** * The unique chatId diff --git a/himarket-server/src/main/java/com/alibaba/himarket/dto/result/chat/LlmInvokeResult.java b/himarket-server/src/main/java/com/alibaba/himarket/dto/result/chat/LlmInvokeResult.java index 1aa863c1a..4a596d71a 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/dto/result/chat/LlmInvokeResult.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/dto/result/chat/LlmInvokeResult.java @@ -19,7 +19,7 @@ package com.alibaba.himarket.dto.result.chat; -import com.alibaba.himarket.dto.params.chat.ChatContext; +import com.alibaba.himarket.service.legacy.ChatContextLegacy; import com.alibaba.himarket.support.chat.ChatUsage; import lombok.Builder; import lombok.Data; @@ -42,7 +42,8 @@ public class LlmInvokeResult { */ private ChatUsage usage; - public static LlmInvokeResult of(ChatContext chatContext) { + @Deprecated + public static LlmInvokeResult of(ChatContextLegacy chatContext) { return LlmInvokeResult.builder() .success(chatContext.getSuccess()) .answer(chatContext.getAnswerContent().toString()) diff --git a/himarket-server/src/main/java/com/alibaba/himarket/dto/result/chat/OpenAIChatStreamResponse.java b/himarket-server/src/main/java/com/alibaba/himarket/dto/result/chat/OpenAIChatStreamResponse.java deleted file mode 100644 index 63b28eeec..000000000 --- a/himarket-server/src/main/java/com/alibaba/himarket/dto/result/chat/OpenAIChatStreamResponse.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package com.alibaba.himarket.dto.result.chat; - -import cn.hutool.core.annotation.Alias; -import com.alibaba.himarket.support.chat.ChatUsage; -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.List; -import java.util.Optional; -import lombok.Data; - -@Data -public class OpenAIChatStreamResponse { - - /** - * Response ID - */ - private String id; - - /** - * Response type, currently always "chat.completion.chunk" - */ - private String object; - - /** - * Timestamp - */ - private Long created; - - /** - * Usage - */ - private Usage usage; - - /** - * Model name - */ - private String model; - - /** - * System fingerprint - */ - @JsonProperty("system_fingerprint") - @Alias("system_fingerprint") - private String systemFingerprint; - - /** - * Choices - */ - private List choices; - - @Data - public static class Choice { - /** - * Delta - */ - private Delta delta; - - /** - * Index - */ - private Integer index; - - /** - * Reason for completion - */ - @JsonProperty("finish_reason") - @Alias("finish_reason") - private String finishReason; - } - - @Data - public static class Delta { - /** - * Role - */ - private String role; - - /** - * Content - */ - private String content; - - /** - * Reasoning content, only returned when finish_reason is "reasoning" - */ - @JsonProperty("reasoning_content") - @Alias("reasoning_content") - private String reasoningContent; - } - - @Data - public static class Usage { - - @JsonProperty("first_byte_timeout") - @Alias("first_byte_timeout") - private Long firstByteTimeout; - - @JsonProperty("elapsed_time") - @Alias("elapsed_time") - private Long elapsedTime; - - /** - * Tokens used for prompt - */ - @JsonProperty("prompt_tokens") - @Alias("prompt_tokens") - private Integer promptTokens; - - /** - * Tokens used for completion - */ - @JsonProperty("completion_tokens") - @Alias("completion_tokens") - private Integer completionTokens; - - /** - * Total tokens used, including prompt and completion - */ - @JsonProperty("total_tokens") - @Alias("total_tokens") - private Integer totalTokens; - - /** - * Tokens used for prompt details - */ - @JsonProperty("prompt_tokens_details") - @Alias("prompt_tokens_details") - private PromptTokensDetails promptTokensDetails; - } - - @Data - public static class PromptTokensDetails { - /** - * Cached tokens - */ - @JsonProperty("cached_tokens") - @Alias("cached_tokens") - private Integer cachedTokens; - } - - public ChatUsage toStandardUsage() { - if (this.usage == null) { - return null; - } - - return ChatUsage.builder() - .promptTokens(this.usage.getPromptTokens()) - .completionTokens(this.usage.getCompletionTokens()) - .totalTokens(this.usage.getTotalTokens()) - .promptTokensDetails( - Optional.ofNullable(this.usage.getPromptTokensDetails()) - .map( - details -> - ChatUsage.PromptTokensDetails.builder() - .cachedTokens(details.getCachedTokens()) - .build()) - .orElse(null)) - .build(); - } -} diff --git a/himarket-server/src/main/java/com/alibaba/himarket/dto/result/model/AIGWModelAPIResult.java b/himarket-server/src/main/java/com/alibaba/himarket/dto/result/model/AIGWModelAPIResult.java index 46098134f..74075dbf0 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/dto/result/model/AIGWModelAPIResult.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/dto/result/model/AIGWModelAPIResult.java @@ -21,7 +21,9 @@ import lombok.Builder; import lombok.Data; +import lombok.EqualsAndHashCode; +@EqualsAndHashCode(callSuper = true) @Data @Builder public class AIGWModelAPIResult extends GatewayModelAPIResult { diff --git a/himarket-server/src/main/java/com/alibaba/himarket/dto/result/model/HigressModelResult.java b/himarket-server/src/main/java/com/alibaba/himarket/dto/result/model/HigressModelResult.java index d4b9ef540..9fd621e53 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/dto/result/model/HigressModelResult.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/dto/result/model/HigressModelResult.java @@ -21,7 +21,9 @@ import lombok.Builder; import lombok.Data; +import lombok.EqualsAndHashCode; +@EqualsAndHashCode(callSuper = true) @Data @Builder public class HigressModelResult extends GatewayModelAPIResult { diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/ConsumerService.java b/himarket-server/src/main/java/com/alibaba/himarket/service/ConsumerService.java index b940678f3..93dc630a0 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/ConsumerService.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/ConsumerService.java @@ -30,6 +30,7 @@ import com.alibaba.himarket.dto.result.consumer.ConsumerResult; import com.alibaba.himarket.dto.result.consumer.CredentialContext; import com.alibaba.himarket.dto.result.product.SubscriptionResult; +import java.util.List; import org.springframework.data.domain.Pageable; public interface ConsumerService { @@ -133,6 +134,14 @@ public interface ConsumerService { PageResult listSubscriptions( String consumerId, QuerySubscriptionParam param, Pageable pageable); + /** + * List subscriptions of a consumer + * + * @param consumerId + * @return + */ + List listConsumerSubscriptions(String consumerId); + /** * Approve a subscription * diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/DeveloperService.java b/himarket-server/src/main/java/com/alibaba/himarket/service/DeveloperService.java index 8e46eb1cd..090aaf1b1 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/DeveloperService.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/DeveloperService.java @@ -19,7 +19,6 @@ package com.alibaba.himarket.service; -import com.alibaba.himarket.core.event.PortalDeletingEvent; import com.alibaba.himarket.dto.params.developer.CreateDeveloperParam; import com.alibaba.himarket.dto.params.developer.CreateExternalDeveloperParam; import com.alibaba.himarket.dto.params.developer.QueryDeveloperParam; @@ -132,13 +131,6 @@ public interface DeveloperService { */ void updateProfile(UpdateDeveloperParam param); - /** - * Clean developer resources when portal is deleted - * - * @param event - */ - void handlePortalDeletion(PortalDeletingEvent event); - /** * Logout * diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/PortalService.java b/himarket-server/src/main/java/com/alibaba/himarket/service/PortalService.java index 388682e38..f2c59f336 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/PortalService.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/PortalService.java @@ -34,122 +34,121 @@ public interface PortalService { /** - * 创建门户 + * Create a portal * - * @param param - * @return + * @param param the portal creation parameters + * @return the created portal result */ PortalResult createPortal(CreatePortalParam param); /** - * 查询门户 + * Get a portal * - * @param portalId - * @return + * @param portalId the portal ID + * @return the portal result */ PortalResult getPortal(String portalId); /** - * 检查门户是否存在 + * Check if portal exists * - * @param portalId + * @param portalId the portal ID + * @throws com.alibaba.himarket.core.exception.BusinessException if portal not found */ void existsPortal(String portalId); /** - * 查询门户列表 + * List portals with pagination * - * @param pageable - * @return + * @param pageable the pagination parameters + * @return page result of portals */ PageResult listPortals(Pageable pageable); /** - * 更新门户 + * Update a portal * - * @param portalId - * @param param - * @return + * @param portalId the portal ID + * @param param the portal update parameters + * @return the updated portal result */ PortalResult updatePortal(String portalId, UpdatePortalParam param); /** - * 删除门户 + * Delete a portal * - * @param portalId + * @param portalId the portal ID */ void deletePortal(String portalId); /** - * 根据请求域名解析门户 + * Resolve portal by domain * - * @param domain - * @return + * @param domain the domain name + * @return the portal ID or null if not found */ String resolvePortal(String domain); /** - * 为门户绑定域名 + * Bind domain to portal * - * @param portalId - * @param param - * @return + * @param portalId the portal ID + * @param param the domain binding parameters + * @return the updated portal result */ PortalResult bindDomain(String portalId, BindDomainParam param); /** - * 删除门户绑定域名 + * Unbind domain from portal * - * @param portalId - * @param domain - * @return + * @param portalId the portal ID + * @param domain the domain name to unbind + * @return the updated portal result */ PortalResult unbindDomain(String portalId, String domain); /** - * 获取门户上的API产品订阅列表 + * Get API product subscription list for portal * - * @param portalId 门户ID - * @param param 查询参数 - * @param pageable 分页参数 - * @return PageResult of SubscriptionResult + * @param portalId the portal ID + * @param param the query parameters + * @param pageable the pagination parameters + * @return page result of subscriptions */ PageResult listSubscriptions( String portalId, QuerySubscriptionParam param, Pageable pageable); /** - * 获取门户已发布的产品列表 + * Get published product list for portal * - * @param portalId 门户ID - * @param pageable 分页参数 - * @return PageResult of ProductPublicationResult + * @param portalId the portal ID + * @param pageable the pagination parameters + * @return page result of product publications */ PageResult getPublications(String portalId, Pageable pageable); /** - * 获取默认门户 + * Get default portal * - * @return + * @return the default portal ID or null if not found */ String getDefaultPortal(); - // ========== 搜索引擎配置查询 ========== - /** - * 根据引擎类型获取 API Key(供搜索功能使用) 核心方法:TalkSearchAbilityServiceGoogleImpl 将调用此方法 + * Get API Key by engine type for search functionality. * - * @param portalId Portal ID - * @param engineType 搜索引擎类型 - * @return API Key(自动解密) - * @throws com.alibaba.himarket.core.exception.BusinessException 如果未配置搜索引擎或搜索引擎未启用 + * @param portalId the portal ID + * @param engineType the search engine type + * @return the API Key (automatically decrypted) + * @throws com.alibaba.himarket.core.exception.BusinessException if search engine is not configured or disabled */ String getSearchEngineApiKey(String portalId, SearchEngineType engineType); /** - * 获取 Portal 的搜索引擎配置(供开发者查询) + * Get search engine configuration for portal (for developer queries) * - * @param portalId Portal ID - * @return 搜索引擎配置,如果未配置则返回 null + * @param portalId the portal ID + * @return the search engine configuration, or null if not configured */ SearchEngineConfig getSearchEngineConfig(String portalId); } diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/ProductService.java b/himarket-server/src/main/java/com/alibaba/himarket/service/ProductService.java index f5e41cb34..965c44281 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/ProductService.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/ProductService.java @@ -19,7 +19,6 @@ package com.alibaba.himarket.service; -import com.alibaba.himarket.core.event.PortalDeletingEvent; import com.alibaba.himarket.dto.params.product.*; import com.alibaba.himarket.dto.result.common.PageResult; import com.alibaba.himarket.dto.result.mcp.McpToolListResult; @@ -124,13 +123,6 @@ public interface ProductService { */ void deleteProductRef(String productId); - /** - * Clean up portal resources - * - * @param event - */ - void handlePortalDeletion(PortalDeletingEvent event); - /** * Get API products, if withConfig is true, additional configuration information will be loaded * including categories, API config, MCP config, agent config and model config diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/gateway/APIGOperator.java b/himarket-server/src/main/java/com/alibaba/himarket/service/gateway/APIGOperator.java index f51e4b380..836a17fe6 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/gateway/APIGOperator.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/gateway/APIGOperator.java @@ -244,8 +244,8 @@ public String createConsumer( APIGClient client = new APIGClient(config.getApigConfig()); String mark = - consumer.getConsumerId() - .substring(Math.max(0, consumer.getConsumerId().length() - 8)); + consumer.getDeveloperId() + .substring(Math.max(0, consumer.getDeveloperId().length() - 8)); String gwConsumerName = StrUtil.format("{}-{}", consumer.getName(), mark); try { // ApiKey diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/gateway/GatewayOperator.java b/himarket-server/src/main/java/com/alibaba/himarket/service/gateway/GatewayOperator.java index 018d1ef27..53a2bedc9 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/gateway/GatewayOperator.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/gateway/GatewayOperator.java @@ -21,6 +21,7 @@ import com.alibaba.himarket.core.exception.BusinessException; import com.alibaba.himarket.core.exception.ErrorCode; +import com.alibaba.himarket.core.utils.CacheUtil; import com.alibaba.himarket.dto.result.agent.AgentAPIResult; import com.alibaba.himarket.dto.result.common.PageResult; import com.alibaba.himarket.dto.result.gateway.GatewayResult; @@ -31,23 +32,22 @@ import com.alibaba.himarket.entity.ConsumerCredential; import com.alibaba.himarket.entity.Gateway; import com.alibaba.himarket.service.gateway.client.APIGClient; -import com.alibaba.himarket.service.gateway.client.ApsaraStackGatewayClient; import com.alibaba.himarket.service.gateway.client.GatewayClient; import com.alibaba.himarket.service.gateway.client.HigressClient; import com.alibaba.himarket.support.consumer.ConsumerAuthConfig; import com.alibaba.himarket.support.enums.GatewayType; import com.alibaba.himarket.support.gateway.GatewayConfig; import com.aliyun.sdk.service.apig20240327.models.HttpApiApiInfo; +import com.github.benmanes.caffeine.cache.Cache; import java.net.URI; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import lombok.extern.slf4j.Slf4j; @Slf4j public abstract class GatewayOperator { - private final Map clientCache = new ConcurrentHashMap<>(); + private final Cache clientCache = + CacheUtil.newCache(60 * 3, GatewayClient::close); public abstract PageResult fetchHTTPAPIs(Gateway gateway, int page, int size); @@ -101,7 +101,7 @@ protected T getClient(Gateway gateway) { gateway.getGatewayType().isAPIG() ? gateway.getApigConfig().buildUniqueKey() : gateway.getHigressConfig().buildUniqueKey(); - return (T) clientCache.computeIfAbsent(clientKey, key -> createClient(gateway)); + return (T) clientCache.get(clientKey, key -> createClient(gateway)); } /** Create a gateway client for the given gateway. */ @@ -111,7 +111,7 @@ private GatewayClient createClient(Gateway gateway) { case APIG_AI: return new APIGClient(gateway.getApigConfig()); case APSARA_GATEWAY: - return new ApsaraStackGatewayClient(gateway.getApsaraGatewayConfig()); + // return new ApsaraStackGatewayClient(gateway.getApsaraGatewayConfig()); case HIGRESS: return new HigressClient(gateway.getHigressConfig()); default: @@ -120,14 +120,4 @@ private GatewayClient createClient(Gateway gateway) { "No factory found for gateway type: " + gateway.getGatewayType()); } } - - /** Remove a gateway client for the given gateway. */ - public void removeClient(String instanceId) { - GatewayClient client = clientCache.remove(instanceId); - try { - client.close(); - } catch (Exception e) { - log.error("Error closing client for instance: {}", instanceId, e); - } - } } diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/gateway/HigressOperator.java b/himarket-server/src/main/java/com/alibaba/himarket/service/gateway/HigressOperator.java index b474042f8..b7606efd3 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/gateway/HigressOperator.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/gateway/HigressOperator.java @@ -44,8 +44,8 @@ import com.alibaba.himarket.entity.ConsumerCredential; import com.alibaba.himarket.entity.Gateway; import com.alibaba.himarket.service.gateway.client.HigressClient; -import com.alibaba.himarket.service.impl.McpClientFactory; -import com.alibaba.himarket.service.impl.McpClientWrapper; +import com.alibaba.himarket.service.hichat.manager.ToolManager; +import com.alibaba.himarket.support.chat.mcp.MCPTransportConfig; import com.alibaba.himarket.support.consumer.ApiKeyConfig; import com.alibaba.himarket.support.consumer.ConsumerAuthConfig; import com.alibaba.himarket.support.consumer.HigressAuthConfig; @@ -54,14 +54,16 @@ import com.alibaba.himarket.support.gateway.HigressConfig; import com.alibaba.himarket.support.product.HigressRefConfig; import com.aliyun.sdk.service.apig20240327.models.HttpApiApiInfo; +import io.agentscope.core.tool.mcp.McpClientWrapper; import io.modelcontextprotocol.spec.McpSchema; -import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.*; import java.util.stream.Collectors; import lombok.Builder; import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.core.ParameterizedTypeReference; @@ -70,8 +72,11 @@ @Service @Slf4j +@RequiredArgsConstructor public class HigressOperator extends GatewayOperator { + private final ToolManager toolManager; + @Override public PageResult fetchHTTPAPIs(Gateway gateway, int page, int size) { throw new UnsupportedOperationException("Higress gateway does not support HTTP APIs"); @@ -371,22 +376,22 @@ public String fetchMcpToolsForConfig(Gateway gateway, Object conf) { credentialContext, credential)); }); - // Get and transform tool list - try (McpClientWrapper mcpClientWrapper = - McpClientFactory.newClient(config.toTransportConfig(), credentialContext)) { - if (mcpClientWrapper == null) { - return null; - } - - List tools = mcpClientWrapper.listTools(); - OpenAPIMCPConfig openAPIMCPConfig = - OpenAPIMCPConfig.convertFromToolList(config.getMcpServerName(), tools); + MCPTransportConfig transportConfig = config.toTransportConfig(); + transportConfig.setHeaders(credentialContext.copyHeaders()); + transportConfig.setQueryParams(credentialContext.copyQueryParams()); - return JSONUtil.toJsonStr(openAPIMCPConfig); - } catch (IOException e) { - log.error("List mcp tools failed", e); + McpClientWrapper mcpClientWrapper = + toolManager.getOrCreateClient(config.toTransportConfig()); + if (mcpClientWrapper == null) { return null; } + + // Get and transform tool list + List tools = mcpClientWrapper.listTools().block(); + OpenAPIMCPConfig openAPIMCPConfig = + OpenAPIMCPConfig.convertFromToolList(config.getMcpServerName(), tools); + + return JSONUtil.toJsonStr(openAPIMCPConfig); } private void fillCredentialContext( @@ -684,6 +689,7 @@ public static class HigressCredential { protected Map properties; } + @EqualsAndHashCode(callSuper = true) @Data public static class HigressKeyAuthCredential extends HigressCredential { private String source; diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/gateway/client/HigressClient.java b/himarket-server/src/main/java/com/alibaba/himarket/service/gateway/client/HigressClient.java index 96bd1f75f..14577bcad 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/gateway/client/HigressClient.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/gateway/client/HigressClient.java @@ -197,9 +197,4 @@ private void login() { .orElseThrow( () -> new RuntimeException("Failed to get Higress session token")); } - - @Override - public void close() { - HTTPClientFactory.closeClient(restTemplate); - } } diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/gateway/factory/HTTPClientFactory.java b/himarket-server/src/main/java/com/alibaba/himarket/service/gateway/factory/HTTPClientFactory.java index 03c3c93b2..d864b6151 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/gateway/factory/HTTPClientFactory.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/gateway/factory/HTTPClientFactory.java @@ -19,42 +19,22 @@ package com.alibaba.himarket.service.gateway.factory; -import java.util.concurrent.TimeUnit; +import java.net.http.HttpClient; +import java.time.Duration; import lombok.extern.slf4j.Slf4j; -import okhttp3.ConnectionPool; -import okhttp3.OkHttpClient; -import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.http.client.OkHttp3ClientHttpRequestFactory; +import org.springframework.http.client.JdkClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; @Slf4j public class HTTPClientFactory { public static RestTemplate createRestTemplate() { - OkHttpClient okHttpClient = okHttpClient(); - // 使用OkHttp作为RestTemplate的底层客户端 - return new RestTemplate(new OkHttp3ClientHttpRequestFactory(okHttpClient)); - } + HttpClient httpClient = + HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build(); - public static OkHttpClient okHttpClient() { - return new OkHttpClient.Builder() - .connectTimeout(5, TimeUnit.SECONDS) - .readTimeout(5, TimeUnit.SECONDS) - .writeTimeout(5, TimeUnit.SECONDS) - .connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES)) - .build(); - } + JdkClientHttpRequestFactory factory = new JdkClientHttpRequestFactory(httpClient); + factory.setReadTimeout(Duration.ofSeconds(5)); - public static void closeClient(RestTemplate restTemplate) { - try { - if (restTemplate != null) { - ClientHttpRequestFactory factory = restTemplate.getRequestFactory(); - if (factory instanceof OkHttp3ClientHttpRequestFactory) { - ((OkHttp3ClientHttpRequestFactory) factory).destroy(); - } - } - } catch (Exception e) { - log.error("Error closing RestTemplate", e); - } + return new RestTemplate(factory); } } diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/manager/ChatBotManager.java b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/manager/ChatBotManager.java new file mode 100644 index 000000000..9e9683531 --- /dev/null +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/manager/ChatBotManager.java @@ -0,0 +1,520 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.alibaba.himarket.service.hichat.manager; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.SecureUtil; +import com.alibaba.himarket.core.event.McpClientRemovedEvent; +import com.alibaba.himarket.core.utils.CacheUtil; +import com.alibaba.himarket.dto.result.product.ProductResult; +import com.alibaba.himarket.service.hichat.support.ChatBot; +import com.alibaba.himarket.service.hichat.support.LlmChatRequest; +import com.alibaba.himarket.service.hichat.support.ToolMeta; +import com.alibaba.himarket.support.chat.mcp.MCPTransportConfig; +import com.github.benmanes.caffeine.cache.Cache; +import io.agentscope.core.ReActAgent; +import io.agentscope.core.memory.InMemoryMemory; +import io.agentscope.core.memory.Memory; +import io.agentscope.core.message.Msg; +import io.agentscope.core.model.Model; +import io.agentscope.core.tool.ToolGroup; +import io.agentscope.core.tool.Toolkit; +import io.agentscope.core.tool.mcp.McpClientWrapper; +import io.agentscope.core.tool.mcp.McpTool; +import io.modelcontextprotocol.spec.McpSchema; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ChatBotManager { + + private final ToolManager toolManager; + private final Cache chatBotCache = CacheUtil.newLRUCache(10 * 60); + + /** + * A reverse lookup map tracking dependencies between tools and ChatBots. + * + *

Structure: + * - Key: Tool key "tool:{md5}" (md5 = hash(url+headers+params)) + * - Value: Set of dependent ChatBot keys + * + *

Used for cascade invalidation when a tool is removed from cache. + */ + private final Map> toolDependencies = new ConcurrentHashMap<>(); + + /** + * Get existing ChatBot or create a new one based on request + * + * @param request chat request containing session and configuration info + * @param model LLM model to be used + * @return ChatBot instance or null if creation fails + */ + public ChatBot getOrCreateChatBot(LlmChatRequest request, Model model) { + String sessionId = request.getSessionId(); + String productId = request.getProduct().getProductId(); + + if (StrUtil.isBlank(sessionId) || StrUtil.isBlank(productId)) { + log.error("Invalid request: sessionId and productId required"); + return null; + } + + String cacheKey = buildCacheKey(request); + + // Check if cached ChatBot exists and is still valid + ChatBot cachedBot = chatBotCache.getIfPresent(cacheKey); + if (cachedBot != null) { + if (cachedBot.isValid()) { + log.debug("Reused ChatBot from cache, degraded: {}", cachedBot.isDegraded()); + return cachedBot; + } else { + // Invalid (degraded TTL exceeded), remove from cache and create a new one + chatBotCache.invalidate(cacheKey); + log.info("ChatBot invalid (degraded TTL exceeded), removed from cache"); + } + } + + // Create a new ChatBot + try { + ChatBot chatBot = createChatBot(request, model); + if (chatBot != null) { + chatBotCache.put(cacheKey, chatBot); + + // Register mapping relationship + int mcpCount = registerToolDependencies(cacheKey, request.getMcpConfigs()); + + log.info( + "Created new ChatBot for session: {}, degraded: {}, MCPs: {}", + sessionId, + chatBot.isDegraded(), + mcpCount); + } + return chatBot; + } catch (Exception e) { + log.error( + "Failed to create ChatBot, sessionId: {}, productId: {}", + sessionId, + productId, + e); + return null; + } + } + + /** + * Create a new ChatBot instance with required components + * + * @param request chat request containing configuration + * @param model LLM model to be used + * @return configured ChatBot instance + */ + private ChatBot createChatBot(LlmChatRequest request, Model model) { + ProductResult product = request.getProduct(); + long startTime = System.currentTimeMillis(); + + // Initialize and register mcp tools + List mcpConfigs = request.getMcpConfigs(); + int expectedMcpCount = CollUtil.isEmpty(mcpConfigs) ? 0 : mcpConfigs.size(); + + List mcpClients = loadMcpClients(request); + Toolkit toolkit = new Toolkit(); + long actualSuccessCount = registerMcpTools(toolkit, mcpClients); + + // Build tool metadata mapping + Map toolMetas = buildToolMetas(toolkit); + + // Initialize memory + Memory memory = createMemory(request.getHistoryMessages()); + String systemPrompt = buildSystemPrompt(product.getName()); + + // Build agent for react chat + ReActAgent agent = + ReActAgent.builder() + .name(product.getName()) + .sysPrompt(systemPrompt) + .model(model) + .toolkit(toolkit) + .memory(memory) + .maxIters(10) + .build(); + + // Determine if ChatBot is in degraded mode + boolean degraded = actualSuccessCount < expectedMcpCount; + + long totalTime = System.currentTimeMillis() - startTime; + log.info( + "ChatBot created successfully for session: {}, MCP: {}/{}, degraded: {}, total" + + " time: {}ms", + request.getSessionId(), + actualSuccessCount, + expectedMcpCount, + degraded, + totalTime); + + return ChatBot.builder().agent(agent).toolMetas(toolMetas).degraded(degraded).build(); + } + + /** + * Load MCP clients from configuration + * + * @param request chat request containing MCP configs + * @return list of MCP client wrappers + */ + private List loadMcpClients(LlmChatRequest request) { + List mcpConfigs = request.getMcpConfigs(); + if (CollUtil.isEmpty(mcpConfigs)) { + log.debug("No MCP configs found for chat: {}", request.getChatId()); + return List.of(); + } + + List clients = toolManager.getOrCreateClients(mcpConfigs); + if (clients.isEmpty()) { + log.warn("No MCP clients available for chat: {}", request.getChatId()); + } + return clients; + } + + /** + * Register MCP tools to toolkit + * + * @param toolkit toolkit to register tools + * @param clients MCP clients containing tools + * @return number of MCP clients that successfully registered tools + */ + private long registerMcpTools(Toolkit toolkit, List clients) { + if (clients.isEmpty()) { + return 0; + } + + long startTime = System.currentTimeMillis(); + + // Process all MCP clients in parallel (max 20 concurrent) + Long result = + Flux.fromIterable(clients) + .flatMap( + client -> { + // Try to list and register tools from this client + return client.listTools() + .flatMapIterable(tools -> tools) + .doOnNext(tool -> registerTool(toolkit, client, tool)) + // Success: count this client + .then(Mono.just(1)) + .doOnError( + error -> + log.error( + "Failed to list tools from MCP" + + " server: {}, error: {}", + client.getName(), + error.getMessage())) + .onErrorResume(error -> Mono.empty()); + }, + 20) + .count() + .defaultIfEmpty(0L) + .block(); + + long successCount = result != null ? result : 0; + long totalTime = System.currentTimeMillis() - startTime; + + log.info( + "MCP tools registered: {}/{} servers succeeded, total time: {}ms", + successCount, + clients.size(), + totalTime); + + return successCount; + } + + /** + * Build tool metadata from toolkit + * + * @param toolkit toolkit containing registered tools + * @return map from tool name to tool metadata + */ + private Map buildToolMetas(Toolkit toolkit) { + Map toolMetas = new HashMap<>(); + + // Get all active groups (each group represents an MCP server) + List activeGroups = toolkit.getActiveGroups(); + + for (String groupName : activeGroups) { + ToolGroup group = toolkit.getToolGroup(groupName); + if (group == null) { + log.warn("Tool group not found: {}", groupName); + continue; + } + + Set tools = group.getTools(); + + for (String toolName : tools) { + ToolMeta toolMeta = + ToolMeta.builder().mcpServerName(groupName).toolName(toolName).build(); + + toolMetas.put(toolName, toolMeta); + } + } + + return toolMetas; + } + + /** + * Register single MCP tool to toolkit with groupName + * + * @param toolkit toolkit to register tool + * @param client MCP client wrapper + * @param tool tool to be registered + */ + private void registerTool(Toolkit toolkit, McpClientWrapper client, McpSchema.Tool tool) { + try { + Map parameters = + McpTool.convertMcpSchemaToParameters(tool.inputSchema()); + McpTool mcpTool = + new McpTool( + tool.name(), + tool.description() != null ? tool.description() : "", + parameters, + client); + + // Use MCP server name as groupName + String groupName = client.getName(); + + // Create tool group if not exists + if (toolkit.getToolGroup(groupName) == null) { + toolkit.createToolGroup(groupName, "Tools from MCP server: " + groupName, true); + } + + // Register tool with group + toolkit.registration().agentTool(mcpTool).group(groupName).apply(); + + } catch (Exception e) { + log.error("Failed to register tool: {} from {}", tool.name(), client.getName(), e); + } + } + + /** + * Create memory with history messages + * + * @param historyMessages list of historical messages + * @return memory instance with loaded messages + */ + private Memory createMemory(List historyMessages) { + Memory memory = new InMemoryMemory(); + + if (CollUtil.isNotEmpty(historyMessages)) { + // Limit initial memory to 20 messages + int maxInitMessages = 20; + List messagesToLoad = historyMessages; + + if (historyMessages.size() > maxInitMessages) { + int startIndex = historyMessages.size() - maxInitMessages; + messagesToLoad = historyMessages.subList(startIndex, historyMessages.size()); + log.info( + "Truncated history from {} to {} messages for initialization", + historyMessages.size(), + maxInitMessages); + } + + messagesToLoad.forEach(memory::addMessage); + log.debug("Initialized memory with {} messages", messagesToLoad.size()); + } + + return memory; + } + + /** + * Build system prompt for ChatBot + * + * @param productName name of the product + * @return formatted system prompt + */ + private String buildSystemPrompt(String productName) { + return String.format( + "You are a helpful AI assistant powered by %s. " + + "You can use various tools to help answer user questions. " + + "Always provide accurate and helpful responses.", + productName); + } + + /** + * Register tool dependencies for cascade invalidation. + * Maps MCP tool keys to dependent ChatBot for automatic cleanup when tool is removed. + * + * @param chatBotCacheKey Cache key of ChatBot to track + * @param mcpConfigs List of MCP configs used by ChatBot + * @return Number of registered tool dependencies + */ + private int registerToolDependencies( + String chatBotCacheKey, List mcpConfigs) { + if (CollUtil.isEmpty(mcpConfigs)) { + return 0; + } + + // Build MCP cache keys + List mcpCacheKeys = mcpConfigs.stream().map(toolManager::buildCacheKey).toList(); + + // Register mapping + for (String mcpCacheKey : mcpCacheKeys) { + toolDependencies + .computeIfAbsent(mcpCacheKey, k -> ConcurrentHashMap.newKeySet()) + .add(chatBotCacheKey); + } + + log.debug( + "Registered mapping for ChatBot: {}, MCP keys: {}, total mappings: {}", + chatBotCacheKey, + mcpCacheKeys.size(), + toolDependencies.size()); + + return mcpCacheKeys.size(); + } + + /** + * Event listener for MCP client removal + * Invalidates all ChatBots that depend on the removed MCP client + * + * @param event MCP client removed event + */ + @EventListener + @Async("taskExecutor") + public void onMcpClientRemoved(McpClientRemovedEvent event) { + String mcpCacheKey = event.getMcpCacheKey(); + + log.info("Received MCP client removed event, key: {}", mcpCacheKey); + + // Get all ChatBot cache keys that depend on this MCP + Set chatBotKeys = toolDependencies.remove(mcpCacheKey); + + if (CollUtil.isEmpty(chatBotKeys)) { + log.debug("No ChatBots depend on MCP key: {}", mcpCacheKey); + return; + } + + // Invalidate all dependent ChatBots directly from cache + for (String cacheKey : chatBotKeys) { + chatBotCache.invalidate(cacheKey); + } + + log.info("Invalidated {} ChatBots for MCP key: {}", chatBotKeys.size(), mcpCacheKey); + } + + /** + * Build cache key from session info, model endpoint and credentials + * + * @param request chat request containing configuration + * @return MD5 hashed cache key + */ + private String buildCacheKey(LlmChatRequest request) { + StringBuilder sb = new StringBuilder(); + + // Session ID (for Memory isolation) + sb.append("session:").append(request.getSessionId()).append("|"); + + // Product ID (for Product isolation) + sb.append("product:").append(request.getProduct().getProductId()).append("|"); + + // Model URL (scheme + host + port + path) + if (request.getUri() != null) { + sb.append("url:") + .append(request.getUri().getScheme()) + .append("://") + .append(request.getUri().getHost()); + + if (request.getUri().getPort() > 0) { + sb.append(":").append(request.getUri().getPort()); + } + + if (StrUtil.isNotBlank(request.getUri().getPath())) { + sb.append(request.getUri().getPath()); + } + + sb.append("|"); + } else { + sb.append("url:none|"); + } + + // Credentials (API Key + Headers + Query Params) + sb.append("cred:"); + + // API Key + if (StrUtil.isNotBlank(request.getApiKey())) { + sb.append("apiKey=").append(request.getApiKey()).append(","); + } + + // Headers (sorted) + if (request.getHeaders() != null && !request.getHeaders().isEmpty()) { + sb.append("headers={"); + request.getHeaders().entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEach( + entry -> + sb.append(entry.getKey()) + .append("=") + .append(entry.getValue()) + .append(",")); + sb.append("},"); + } + + // Query Params (sorted) + if (request.getQueryParams() != null && !request.getQueryParams().isEmpty()) { + sb.append("params={"); + request.getQueryParams().entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEach( + entry -> + sb.append(entry.getKey()) + .append("=") + .append(entry.getValue()) + .append(",")); + sb.append("}"); + } + + sb.append("|"); + + // MCP Tool URLs (sorted) + sb.append("mcp:"); + if (CollUtil.isNotEmpty(request.getMcpConfigs())) { + String mcpUrls = + request.getMcpConfigs().stream() + .map(MCPTransportConfig::getUrl) + .filter(StrUtil::isNotBlank) + .sorted() + .collect(Collectors.joining(",")); + sb.append(mcpUrls); + } else { + sb.append("none"); + } + + // Hash the final string for fixed-length cache key + String rawKey = sb.toString(); + + return "chatBot:" + SecureUtil.md5(rawKey); + } +} diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/manager/ToolManager.java b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/manager/ToolManager.java new file mode 100644 index 000000000..e474ab6e7 --- /dev/null +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/manager/ToolManager.java @@ -0,0 +1,312 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.alibaba.himarket.service.hichat.manager; + +import static reactor.core.scheduler.Schedulers.boundedElastic; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.alibaba.himarket.core.event.McpClientRemovedEvent; +import com.alibaba.himarket.core.utils.CacheUtil; +import com.alibaba.himarket.support.chat.mcp.MCPTransportConfig; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.RemovalCause; +import io.agentscope.core.tool.mcp.McpClientBuilder; +import io.agentscope.core.tool.mcp.McpClientWrapper; +import java.time.Duration; +import java.util.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * Tool manager for MCP client management. + * Handles client creation, caching and initialization. + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ToolManager { + + private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(30); + private static final Duration INITIALIZE_TIMEOUT = Duration.ofSeconds(30); + + // MCP client cache with removal listener (10 minutes = 600 seconds) + private final Cache clientCache = + CacheUtil.newLRUCache(10 * 60, this::onClientRemoved); + + /** + * Get existing client or create new one for MCP config + * + * @param config MCP transport configuration + * @return MCP client wrapper, null if creation fails + */ + public McpClientWrapper getOrCreateClient(MCPTransportConfig config) { + String cacheKey = buildCacheKey(config); + + return clientCache + .asMap() + .computeIfAbsent( + cacheKey, + key -> { + try { + McpClientWrapper client = createClient(config); + if (client != null) { + return client; + } + log.error("Create MCP client failed for: {}", config.getUrl()); + } catch (Exception e) { + log.error("Create MCP client error for: {}", config.getUrl(), e); + } + return null; + }); + } + + /** + * Get or create multiple MCP clients in parallel + * + * @param configs List of MCP transport configurations + * @return List of created MCP client wrappers + */ + public List getOrCreateClients(List configs) { + if (CollUtil.isEmpty(configs)) { + return CollUtil.empty(List.class); + } + + long startTime = System.currentTimeMillis(); + + List result = + Flux.fromIterable(configs) + .flatMap(this::getClient, 20) + .collectList() + .onErrorReturn(Collections.emptyList()) + .block(); + + long totalTime = System.currentTimeMillis() - startTime; + log.info( + "MCP clients initialized: {}/{} succeeded, total time: {}ms", + result.size(), + configs.size(), + totalTime); + + return result; + } + + /** + * Get cached client or create new one reactively + * + *

Uses computeIfAbsent to ensure atomic creation and prevent race conditions + * where multiple threads might create duplicate clients for the same key. + * + * @param config MCP transport configuration + * @return Mono of MCP client wrapper + */ + private Mono getClient(MCPTransportConfig config) { + String cacheKey = buildCacheKey(config); + String serverName = config.getMcpServerName(); + + // Use computeIfAbsent for atomic check-and-create to prevent race conditions + return Mono.fromCallable( + () -> + clientCache + .asMap() + .computeIfAbsent( + cacheKey, + key -> { + try { + return createClient(config); + } catch (Exception e) { + log.error( + "Failed to create MCP client for" + + " server: {}, error: {}", + serverName, + e.getMessage()); + return null; + } + })) + .subscribeOn(boundedElastic()) + .flatMap( + client -> { + if (client != null) { + log.debug("MCP client ready for server: {}", serverName); + return Mono.just(client); + } else { + log.warn( + "MCP client creation returned null for server: {}", + serverName); + return Mono.empty(); + } + }); + } + + /** + * Create new MCP client with config + * + * @param config MCP transport configuration + * @return MCP client wrapper, null if creation fails + */ + public McpClientWrapper createClient(MCPTransportConfig config) { + String serverName = config.getMcpServerName(); + long startTime = System.currentTimeMillis(); + + McpClientBuilder builder = McpClientBuilder.create(serverName).timeout(REQUEST_TIMEOUT); + switch (config.getTransportMode()) { + case SSE: + builder.sseTransport(config.getUrl()); + break; + case STREAMABLE_HTTP: + builder.streamableHttpTransport(config.getUrl()); + break; + default: + throw new IllegalArgumentException( + "Unsupported transport: " + config.getTransportMode()); + } + + // Apply authentication headers and query parameters + if (MapUtil.isNotEmpty(config.getHeaders())) { + builder.headers(config.getHeaders()); + } + if (MapUtil.isNotEmpty(config.getQueryParams())) { + builder.queryParams(config.getQueryParams()); + } + + McpClientWrapper clientWrapper = null; + try { + // Build and initialize client + clientWrapper = builder.buildAsync().block(); + + if (clientWrapper == null) { + log.error("Failed to build MCP client for server: {}", serverName); + return null; + } + + clientWrapper.initialize().timeout(INITIALIZE_TIMEOUT).block(); + + long totalTime = System.currentTimeMillis() - startTime; + log.info("MCP client created for server: {}, total time: {}ms", serverName, totalTime); + + return clientWrapper; + + } catch (Exception e) { + long totalTime = System.currentTimeMillis() - startTime; + log.error( + "Failed to create MCP client for server: {}, time: {}ms, error: {}", + serverName, + totalTime, + e.getMessage()); + + // Clean up failed client + if (clientWrapper != null) { + try { + clientWrapper.close(); + } catch (Exception closeException) { + log.warn("Failed to close MCP client for server: {}", serverName); + } + } + return null; + } + } + + /** + * Build cache key from URL, headers and query params + * Public method for use by other components (e.g. ChatBotManager) + * + * @param config MCP transport configuration + * @return MD5 hashed cache key in format "tool:{md5}" + */ + public String buildCacheKey(MCPTransportConfig config) { + StringBuilder sb = new StringBuilder(); + + // MCP Server URL + sb.append("url:").append(config.getUrl()).append("|"); + + // Credentials (Headers + Query Params) + sb.append("cred:"); + + // Headers (sorted) + if (config.getHeaders() != null && !config.getHeaders().isEmpty()) { + sb.append("headers={"); + config.getHeaders().entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEach( + entry -> + sb.append(entry.getKey()) + .append("=") + .append(entry.getValue()) + .append(",")); + sb.append("},"); + } + + // Query Params (sorted) + if (config.getQueryParams() != null && !config.getQueryParams().isEmpty()) { + sb.append("params={"); + config.getQueryParams().entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEach( + entry -> + sb.append(entry.getKey()) + .append("=") + .append(entry.getValue()) + .append(",")); + sb.append("}"); + } + + // Hash the final string for fixed-length cache key + String rawKey = sb.toString(); + String key = "tool:" + SecureUtil.md5(rawKey); + + log.debug("MCP client cache key built - raw length: {}, key: {}", rawKey.length(), key); + + return key; + } + + /** + * Callback when MCP client is removed from cache + * + * @param cacheKey cache key of the removed client + * @param client the removed MCP client wrapper + * @param cause reason for removal + */ + private void onClientRemoved(String cacheKey, McpClientWrapper client, RemovalCause cause) { + if (client == null) { + return; + } + + log.info( + "MCP client removed from cache: {}, key: {}, cause: {}", + client.getName(), + cacheKey, + cause); + + // Publish event with cache key to notify dependent components + SpringUtil.getApplicationContext().publishEvent(new McpClientRemovedEvent(cacheKey)); + + // Close the MCP client + try { + client.close(); + log.info("MCP client closed: {}", client.getName()); + } catch (Exception e) { + log.error("Failed to close MCP client: {}", client.getName(), e); + } + } +} diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/service/AbstractLlmService.java b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/service/AbstractLlmService.java new file mode 100644 index 000000000..e9f341d1a --- /dev/null +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/service/AbstractLlmService.java @@ -0,0 +1,197 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.alibaba.himarket.service.hichat.service; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.himarket.core.exception.ChatError; +import com.alibaba.himarket.core.utils.CacheUtil; +import com.alibaba.himarket.dto.result.chat.LlmInvokeResult; +import com.alibaba.himarket.dto.result.consumer.CredentialContext; +import com.alibaba.himarket.dto.result.product.ProductResult; +import com.alibaba.himarket.service.GatewayService; +import com.alibaba.himarket.service.hichat.manager.ChatBotManager; +import com.alibaba.himarket.service.hichat.support.*; +import com.alibaba.himarket.support.product.ModelFeature; +import com.alibaba.himarket.support.product.ProductFeature; +import com.github.benmanes.caffeine.cache.Cache; +import io.agentscope.core.model.Model; +import java.net.URI; +import java.util.*; +import java.util.function.Consumer; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Flux; + +@Slf4j +@RequiredArgsConstructor +public abstract class AbstractLlmService implements LlmService { + + protected final GatewayService gatewayService; + + protected final ChatBotManager chatBotManager; + + private final Cache> gatewayUriCache = CacheUtil.newCache(5 * 60); + + @Override + public Flux invokeLlm( + InvokeModelParam param, Consumer resultHandler) { + + // Create context to collect answer and usage + ChatContext chatContext = new ChatContext(param.getChatId()); + + try { + LlmChatRequest request = composeRequest(param); + // request.tryResolveDns(); + + Model chatModel = newChatModel(request); + ChatBot chatBot = chatBotManager.getOrCreateChatBot(request, chatModel); + chatContext.setToolMetas(chatBot.getToolMetas()); + + ChatFormatter formatter = new ChatFormatter(); + + // Start estimate time and collect answer + chatContext.start(); + return Flux.concat( + // Emit START event + Flux.just(ChatEvent.start(param.getChatId())), + + // Stream chat events with error handling + applyErrorHandling( + chatBot.chat(param.getUserMessage()) + .flatMap(event -> formatter.format(event, chatContext)) + // Collect answer content + .doOnNext(chatContext::collect), + param.getChatId(), + chatContext)) + // Always emit DONE at the end + .concatWith( + Flux.defer( + () -> { + chatContext.stop(); + return Flux.just( + ChatEvent.done( + param.getChatId(), chatContext.getUsage())); + })) + // Unified result handling for all completion scenarios + .doFinally(signal -> resultHandler.accept(chatContext.toResult())); + + } catch (Exception e) { + log.error("Failed to process chat request for chatId: {}", param.getChatId(), e); + ChatError chatError = ChatError.from(e); + chatContext.fail(); + chatContext.appendAnswer("[Sorry, something went wrong: " + e.getMessage() + "]"); + resultHandler.accept(chatContext.toResult()); + + return Flux.just( + ChatEvent.start(param.getChatId()), + ChatEvent.error( + param.getChatId(), + chatError.name(), + StrUtil.blankToDefault(e.getMessage(), chatError.getDescription())), + ChatEvent.done(param.getChatId(), null)); + } + } + + private Flux applyErrorHandling( + Flux flux, String chatId, ChatContext chatContext) { + return flux.doOnCancel( + () -> { + log.warn("Chat stream was canceled by client, chatId: {}", chatId); + chatContext.fail(); + }) + .doOnError( + error -> { + log.error("Chat stream encountered error, chatId: {}", chatId, error); + chatContext.fail(); + chatContext.appendAnswer( + "\n[Sorry, an error occurred: " + error.getMessage() + "]"); + }) + .onErrorResume( + error -> { + ChatError chatError = ChatError.from(error); + log.error( + "Chat execution failed, chatId: {}, errorType: {}", + chatId, + chatError, + error); + + return Flux.just( + ChatEvent.error( + chatId, + chatError.name(), + StrUtil.blankToDefault( + error.getMessage(), + chatError.getDescription()))); + }); + } + + protected LlmChatRequest composeRequest(InvokeModelParam param) { + ProductResult product = param.getProduct(); + + // Get gateway uris for model + List gatewayUris = + gatewayUriCache.get(param.getGatewayId(), gatewayService::fetchGatewayUris); + CredentialContext credentialContext = param.getCredentialContext(); + + return LlmChatRequest.builder() + .chatId(param.getChatId()) + .sessionId(param.getSessionId()) + .product(product) + .userMessages(param.getUserMessage()) + .historyMessages(param.getHistoryMessages()) + .apiKey(credentialContext.getApiKey()) + // Clone headers and query params + .headers(credentialContext.copyHeaders()) + .queryParams(credentialContext.copyQueryParams()) + .gatewayUris(gatewayUris) + .mcpConfigs(param.getMcpConfigs()) + .build(); + } + + protected ModelFeature getOrDefaultModelFeature(ProductResult product) { + ModelFeature modelFeature = + Optional.ofNullable(product) + .map(ProductResult::getFeature) + .map(ProductFeature::getModelFeature) + .orElseGet(() -> ModelFeature.builder().build()); + + return ModelFeature.builder() + .model(StrUtil.blankToDefault(modelFeature.getModel(), "qwen-max")) + .maxTokens(ObjectUtil.defaultIfNull(modelFeature.getMaxTokens(), 5000)) + .temperature(ObjectUtil.defaultIfNull(modelFeature.getTemperature(), 0.9)) + .streaming(ObjectUtil.defaultIfNull(modelFeature.getStreaming(), true)) + .webSearch(ObjectUtil.defaultIfNull(modelFeature.getWebSearch(), false)) + .build(); + } + + @Override + public boolean match(String protocol) { + return getProtocols().stream() + .anyMatch(p -> StrUtil.equalsIgnoreCase(p.getProtocol(), protocol)); + } + + /** + * Create a protocol-specific chat model instance + * + * @param request request containing model config, credentials, and parameters + * @return model instance (e.g. DashScopeChatModel, OpenAIChatModel) + */ + abstract Model newChatModel(LlmChatRequest request); +} diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/service/ChatService.java b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/service/ChatService.java new file mode 100644 index 000000000..f2b0a5bd4 --- /dev/null +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/service/ChatService.java @@ -0,0 +1,445 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.alibaba.himarket.service.hichat.service; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.himarket.core.event.ChatSessionDeletingEvent; +import com.alibaba.himarket.core.exception.BusinessException; +import com.alibaba.himarket.core.exception.ErrorCode; +import com.alibaba.himarket.core.security.ContextHolder; +import com.alibaba.himarket.core.utils.IdGenerator; +import com.alibaba.himarket.dto.params.chat.CreateChatParam; +import com.alibaba.himarket.dto.result.chat.LlmInvokeResult; +import com.alibaba.himarket.dto.result.consumer.CredentialContext; +import com.alibaba.himarket.dto.result.product.ProductRefResult; +import com.alibaba.himarket.dto.result.product.ProductResult; +import com.alibaba.himarket.dto.result.product.SubscriptionResult; +import com.alibaba.himarket.entity.Chat; +import com.alibaba.himarket.entity.ChatAttachment; +import com.alibaba.himarket.entity.ChatSession; +import com.alibaba.himarket.repository.ChatAttachmentRepository; +import com.alibaba.himarket.repository.ChatRepository; +import com.alibaba.himarket.service.*; +import com.alibaba.himarket.service.hichat.support.ChatEvent; +import com.alibaba.himarket.service.hichat.support.InvokeModelParam; +import com.alibaba.himarket.support.chat.attachment.ChatAttachmentConfig; +import com.alibaba.himarket.support.chat.mcp.MCPTransportConfig; +import com.alibaba.himarket.support.enums.ChatAttachmentType; +import com.alibaba.himarket.support.enums.ChatStatus; +import com.alibaba.himarket.support.enums.ProductType; +import io.agentscope.core.message.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.data.domain.Sort; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import reactor.core.publisher.Flux; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ChatService { + + private final ChatSessionService sessionService; + + private final List llmServices; + + private final ChatRepository chatRepository; + + private final ChatAttachmentRepository chatAttachmentRepository; + + private final ContextHolder contextHolder; + + private final ProductService productService; + + private final ConsumerService consumerService; + + public Flux chat(CreateChatParam param) { + performAllChecks(param); + + Chat chat = createChat(param); + InvokeModelParam invokeModelParam = buildInvokeModelParam(param, chat); + + return getLlmService(invokeModelParam) + .invokeLlm(invokeModelParam, r -> updateChatResult(chat.getChatId(), r)); + } + + private void updateChatResult(String chatId, LlmInvokeResult result) { + chatRepository + .findByChatId(chatId) + .ifPresent( + chat -> { + chat.setAnswer(result.getAnswer()); + chat.setStatus( + result.isSuccess() ? ChatStatus.SUCCESS : ChatStatus.FAILED); + chat.setChatUsage(result.getUsage()); + chatRepository.save(chat); + }); + } + + private void performAllChecks(CreateChatParam param) { + ChatSession session = sessionService.findUserSession(param.getSessionId()); + + if (!session.getProducts().contains(param.getProductId())) { + throw new BusinessException( + ErrorCode.INVALID_REQUEST, + StrUtil.format("Product `{}` not in current session", param.getProductId())); + } + + if (CollUtil.isNotEmpty(param.getMcpProducts())) { + Set subscribedProductIds = + consumerService + .listConsumerSubscriptions( + consumerService.getPrimaryConsumer().getConsumerId()) + .stream() + .map(SubscriptionResult::getProductId) + .collect(Collectors.toSet()); + + Set unsubscribedProducts = + param.getMcpProducts().stream() + .filter(productId -> !subscribedProductIds.contains(productId)) + .collect(Collectors.toSet()); + + if (!unsubscribedProducts.isEmpty()) { + log.warn( + "MCP products `{}` are not subscribed, which may cause unauthorized access", + unsubscribedProducts); + } + } + + // chat count + + // check edit + + // check once more + } + + public Chat createChat(CreateChatParam param) { + String chatId = IdGenerator.genChatId(); + Chat chat = param.convertTo(); + chat.setChatId(chatId); + chat.setUserId(contextHolder.getUser()); + + // Sequence represent the number of tries for this question + Integer sequence = + chatRepository.findCurrentSequence( + param.getSessionId(), + param.getConversationId(), + param.getQuestionId(), + param.getProductId()); + chat.setSequence(sequence + 1); + + return chatRepository.save(chat); + } + + private InvokeModelParam buildInvokeModelParam(CreateChatParam param, Chat chat) { + // Get product config + ProductResult productResult = productService.getProduct(param.getProductId()); + + // Record target gateway + ProductRefResult productRef = productService.getProductRef(param.getProductId()); + String gatewayId = productRef.getGatewayId(); + + // Get authentication info + CredentialContext credentialContext = + consumerService.getDefaultCredential(contextHolder.getUser()); + + // Build user msg and history msg list which will be passed to model + List historyMsgList = buildHistoryMsgList(param); + Msg currentMsg = buildUserMsg(chat); + + return InvokeModelParam.builder() + .chatId(chat.getChatId()) + .sessionId(param.getSessionId()) + .userMessage(currentMsg) + .product(productResult) + .historyMessages(historyMsgList) + .enableWebSearch(param.getEnableWebSearch()) + .gatewayId(gatewayId) + .mcpConfigs(buildMCPConfigs(param, credentialContext)) + .credentialContext(credentialContext) + .build(); + } + + public List buildHistoryMsgList(CreateChatParam param) { + List messages = new ArrayList<>(); + + // 1. Query successful chat records from database + List chats = + chatRepository.findBySessionIdAndStatus( + param.getSessionId(), + ChatStatus.SUCCESS, + Sort.by(Sort.Direction.ASC, "createAt")); + + if (CollUtil.isEmpty(chats)) { + return CollUtil.empty(List.class); + } + + // 2. Filter and group chats + Map> chatGroups = + chats.stream() + // Filter valid chats (must have both question and answer) + .filter( + chat -> + StrUtil.isNotBlank(chat.getQuestion()) + && StrUtil.isNotBlank(chat.getAnswer())) + // Exclude current conversation + .filter(chat -> !param.getConversationId().equals(chat.getConversationId())) + // Ensure same product + .filter(chat -> StrUtil.equals(chat.getProductId(), param.getProductId())) + .collect(Collectors.groupingBy(Chat::getConversationId)); + + // 3. Get latest answer for each conversation + // Note: A conversation may have multiple chats for the same question (retries, + // regenerations) + // We need to find the latest question, then get its latest answer + List latestChats = + chatGroups.values().stream() + .map( + conversationChats -> { + // 3.1 Find the latest question ID + String latestQuestionId = + conversationChats.stream() + .max(Comparator.comparing(Chat::getCreateAt)) + .map(Chat::getQuestionId) + .orElse(null); + + if (StrUtil.isBlank(latestQuestionId)) { + return null; + } + + // 3.2 Get the latest answer for this question + return conversationChats.stream() + .filter( + chat -> + latestQuestionId.equals( + chat.getQuestionId())) + .max(Comparator.comparing(Chat::getCreateAt)) + .orElse(null); + }) + .filter(Objects::nonNull) + .sorted(Comparator.comparing(Chat::getCreateAt)) + .toList(); + + // 4. Build AgentScope Msg objects (user + assistant pairs) + for (Chat chat : latestChats) { + // User message (with multimodal support) + Msg userMsg = buildUserMsg(chat); + messages.add(userMsg); + + // Assistant message + Msg assistantMsg = buildAssistantMsg(chat); + messages.add(assistantMsg); + } + + // 5. Truncate if too many messages + messages = truncateMessages(messages); + + log.debug( + "Built {} AgentScope messages from {} conversations for session: {}", + messages.size(), + latestChats.size(), + param.getSessionId()); + return messages; + } + + private Msg buildUserMsg(Chat chat) { + List contentBlocks = new ArrayList<>(); + + // 1. Prepare text content (question) + StringBuilder textContent = new StringBuilder(); + if (StrUtil.isNotBlank(chat.getQuestion())) { + textContent.append(chat.getQuestion()); + } + + // 2. Load and process attachments + List attachmentConfigs = chat.getAttachments(); + if (CollUtil.isNotEmpty(attachmentConfigs)) { + List attachmentIds = + attachmentConfigs.stream() + .map(ChatAttachmentConfig::getAttachmentId) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toList()); + + if (CollUtil.isNotEmpty(attachmentIds)) { + List attachments = + chatAttachmentRepository.findByAttachmentIdIn(attachmentIds); + + for (ChatAttachment attachment : attachments) { + if (attachment == null || ArrayUtil.isEmpty(attachment.getData())) { + continue; + } + + // Process attachment based on type + if (attachment.getType() == ChatAttachmentType.TEXT) { + buildTextContent(attachment, textContent); + } else { + // IMAGE, AUDIO, VIDEO + buildMediaContent(attachment, contentBlocks); + } + } + } + } + + // 3. Build content blocks + // Always add text content first (using correct AgentScope API) + if (!textContent.isEmpty()) { + contentBlocks.add(0, TextBlock.builder().text(textContent.toString()).build()); + } + + // 4. Create Msg object with proper content format + // If no content blocks, use textContent() convenience method for empty string + if (contentBlocks.isEmpty()) { + return Msg.builder().role(MsgRole.USER).textContent("").build(); + } else { + return Msg.builder().role(MsgRole.USER).content(contentBlocks).build(); + } + } + + private void buildTextContent(ChatAttachment attachment, StringBuilder textContent) { + String text = new String(attachment.getData(), StandardCharsets.UTF_8); + textContent.append("\n\n## ").append(attachment.getName()).append("\n").append(text); + } + + private void buildMediaContent(ChatAttachment attachment, List contentBlocks) { + + // Encode to pure Base64 string (no data URL prefix) + String base64Data = Base64.encode(attachment.getData()); + + // Use default mime type if not specified + String mediaType = + StrUtil.isBlank(attachment.getMimeType()) + ? "application/octet-stream" + : attachment.getMimeType(); + + // Create Base64Source with pure base64 data + Base64Source source = Base64Source.builder().data(base64Data).mediaType(mediaType).build(); + + ContentBlock contentBlock; + switch (attachment.getType()) { + case IMAGE: + contentBlock = ImageBlock.builder().source(source).build(); + break; + case AUDIO: + contentBlock = AudioBlock.builder().source(source).build(); + break; + case VIDEO: + contentBlock = VideoBlock.builder().source(source).build(); + break; + default: + log.warn("Unsupported media attachment type: {}", attachment.getType()); + return; + } + + contentBlocks.add(contentBlock); + } + + private Msg buildAssistantMsg(Chat chat) { + String answer = StrUtil.isBlank(chat.getAnswer()) ? "" : chat.getAnswer(); + // Use textContent() convenience method for simple text messages + return Msg.builder().role(MsgRole.ASSISTANT).textContent(answer).build(); + } + + private List truncateMessages(List messages) { + // Max conversation pairs to keep + int maxHistoryPairs = 10; + int maxMessages = maxHistoryPairs * 2; + + if (messages.size() > maxMessages) { + int startIndex = messages.size() - maxMessages; + List truncated = messages.subList(startIndex, messages.size()); + log.debug("Truncated history to last {} conversation pairs", maxHistoryPairs); + return truncated; + } + + return messages; + } + + private List buildMCPConfigs( + CreateChatParam param, CredentialContext credentialContext) { + if (CollUtil.isEmpty(param.getMcpProducts())) { + return CollUtil.empty(List.class); + } + + return productService.getProducts(param.getMcpProducts(), true).values().stream() + .filter( + product -> + product.getType() == ProductType.MCP_SERVER + || product.getMcpConfig() != null) + .map( + product -> { + MCPTransportConfig transportConfig = + product.getMcpConfig().toTransportConfig(); + + // Add authentication credentials + transportConfig.setHeaders(credentialContext.copyHeaders()); + transportConfig.setQueryParams(credentialContext.copyQueryParams()); + + return transportConfig; + }) + .collect(Collectors.toList()); + } + + private LlmService getLlmService(InvokeModelParam param) { + // Get supported protocols from model config (not null) + List aiProtocols = + param.getProduct().getModelConfig().getModelAPIConfig().getAiProtocols(); + + // Find first matched service by protocol + return llmServices.stream() + .filter(service -> aiProtocols.stream().anyMatch(service::match)) + .findFirst() + .orElseThrow( + () -> + new IllegalArgumentException( + "No supported LLM service found for protocols: " + + aiProtocols)); + } + + /** + * Handle session deletion event - cleanup all related chat records + * + * @param event session deletion event + */ + @EventListener + @Async("taskExecutor") + @Transactional + public void onSessionDeletion(ChatSessionDeletingEvent event) { + String sessionId = event.getSessionId(); + + try { + log.info("Cleaning chat records and attachments for session: {}", sessionId); + + // Delete all chat records + chatRepository.deleteAllBySessionId(sessionId); + + log.info("Successfully cleaned chat records for session: {}", sessionId); + } catch (Exception e) { + log.error("Failed to cleanup chat records for session: {}", sessionId, e); + } + } +} diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/service/DashScopeLlmService.java b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/service/DashScopeLlmService.java new file mode 100644 index 000000000..3495e1d64 --- /dev/null +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/service/DashScopeLlmService.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.alibaba.himarket.service.hichat.service; + +import com.alibaba.himarket.dto.result.model.ModelConfigResult; +import com.alibaba.himarket.dto.result.product.ProductResult; +import com.alibaba.himarket.service.GatewayService; +import com.alibaba.himarket.service.hichat.manager.ChatBotManager; +import com.alibaba.himarket.service.hichat.support.InvokeModelParam; +import com.alibaba.himarket.service.hichat.support.LlmChatRequest; +import com.alibaba.himarket.support.enums.AIProtocol; +import com.alibaba.himarket.support.product.ModelFeature; +import io.agentscope.core.model.DashScopeChatModel; +import io.agentscope.core.model.GenerateOptions; +import io.agentscope.core.model.Model; +import java.net.URI; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class DashScopeLlmService extends AbstractLlmService { + + public DashScopeLlmService(GatewayService gatewayService, ChatBotManager chatBotManager) { + super(gatewayService, chatBotManager); + } + + @Override + protected LlmChatRequest composeRequest(InvokeModelParam param) { + LlmChatRequest request = super.composeRequest(param); + ProductResult product = param.getProduct(); + + if (product.getModelConfig() != null) { + URI uri = getUri(product.getModelConfig(), request.getGatewayUris()); + request.setUri(uri); + } + + return request; + } + + @Override + public Model newChatModel(LlmChatRequest request) { + // Build GenerateOptions with additional parameters + GenerateOptions.Builder optionsBuilder = + GenerateOptions.builder() + .additionalHeaders(request.getHeaders()) + .additionalQueryParams(request.getQueryParams()) + .additionalBodyParams(request.getBodyParams()); + + GenerateOptions options = optionsBuilder.build(); + + ModelFeature modelFeature = getOrDefaultModelFeature(request.getProduct()); + + // TODO set dashscope request uri + + // Build DashScopeChatModel using Builder pattern + return DashScopeChatModel.builder() + .apiKey(request.getApiKey()) + .modelName(modelFeature.getModel()) + .enableSearch(modelFeature.getWebSearch()) + .stream(true) + .defaultOptions(options) + .build(); + } + + private URI getUri(ModelConfigResult modelConfig, List gatewayUris) { + return null; + } + + @Override + public List getProtocols() { + return List.of(AIProtocol.DASHSCOPE, AIProtocol.DASHSCOPE_IMAGE); + } +} diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/service/LlmService.java b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/service/LlmService.java new file mode 100644 index 000000000..7f0d6738b --- /dev/null +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/service/LlmService.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.alibaba.himarket.service.hichat.service; + +import com.alibaba.himarket.dto.result.chat.LlmInvokeResult; +import com.alibaba.himarket.service.hichat.support.ChatEvent; +import com.alibaba.himarket.service.hichat.support.InvokeModelParam; +import com.alibaba.himarket.support.enums.AIProtocol; +import java.util.List; +import java.util.function.Consumer; +import reactor.core.publisher.Flux; + +public interface LlmService { + + /** + * Invoke LLM model for chat and return streaming events + * + * @param param model invocation parameters + * @param resultHandler callback to handle final result (usage, tokens, etc.) + * @return flux of chat events (START, CONTENT, TOOL_CALL, END, etc.) + */ + Flux invokeLlm(InvokeModelParam param, Consumer resultHandler); + + /** + * Get AI protocols supported by this service + * + * @return list of supported protocols (e.g. OPENAI, DASHSCOPE) + */ + List getProtocols(); + + /** + * Check if this service supports the given protocol + * + * @param protocol protocol string to match (case-insensitive) + * @return true if supported + */ + boolean match(String protocol); +} diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/service/OpenAILlmService.java b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/service/OpenAILlmService.java new file mode 100644 index 000000000..efb00f918 --- /dev/null +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/service/OpenAILlmService.java @@ -0,0 +1,171 @@ +package com.alibaba.himarket.service.hichat.service; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.himarket.dto.result.common.DomainResult; +import com.alibaba.himarket.dto.result.httpapi.HttpRouteResult; +import com.alibaba.himarket.dto.result.model.ModelConfigResult; +import com.alibaba.himarket.dto.result.product.ProductResult; +import com.alibaba.himarket.service.GatewayService; +import com.alibaba.himarket.service.hichat.manager.ChatBotManager; +import com.alibaba.himarket.service.hichat.support.InvokeModelParam; +import com.alibaba.himarket.service.hichat.support.LlmChatRequest; +import com.alibaba.himarket.support.enums.AIProtocol; +import com.alibaba.himarket.support.product.ModelFeature; +import io.agentscope.core.formatter.openai.OpenAIChatFormatter; +import io.agentscope.core.model.GenerateOptions; +import io.agentscope.core.model.Model; +import io.agentscope.core.model.OpenAIChatModel; +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.util.UriComponentsBuilder; + +@Service +@Slf4j +public class OpenAILlmService extends AbstractLlmService { + + public OpenAILlmService(GatewayService gatewayService, ChatBotManager chatBotManager) { + super(gatewayService, chatBotManager); + } + + @Override + protected LlmChatRequest composeRequest(InvokeModelParam param) { + LlmChatRequest request = super.composeRequest(param); + ProductResult product = param.getProduct(); + + // Request URI (without query params) + URI uri = getUri(product.getModelConfig(), request.getGatewayUris()); + request.setUri(uri); + + if (BooleanUtil.isTrue(param.getEnableWebSearch())) { + Map webSearchOptions = + JSONUtil.parseObj( + """ + { + "web_search_options": { + "search_context_size": "medium" + } + } + """); + request.setBodyParams(webSearchOptions); + } + + return request; + } + + @Override + public Model newChatModel(LlmChatRequest request) { + URI uri = request.getUri(); + + String baseUrl = + uri.getScheme() + + "://" + + uri.getHost() + + (uri.getPort() == -1 ? "" : ":" + uri.getPort()) + + uri.getPath(); + + ModelFeature modelFeature = getOrDefaultModelFeature(request.getProduct()); + GenerateOptions options = + GenerateOptions.builder().stream(true) + .temperature(modelFeature.getTemperature()) + .maxTokens(modelFeature.getMaxTokens()) + .additionalHeaders(request.getHeaders()) + .additionalQueryParams(request.getQueryParams()) + .additionalBodyParams(request.getBodyParams()) + .build(); + + return OpenAIChatModel.builder() + .baseUrl(baseUrl) + .apiKey(request.getApiKey()) + .modelName(modelFeature.getModel()) + .stream(true) + .formatter(new OpenAIChatFormatter()) + .generateOptions(options) + .build(); + } + + private URI getUri(ModelConfigResult modelConfig, List gatewayUris) { + ModelConfigResult.ModelAPIConfig modelAPIConfig = modelConfig.getModelAPIConfig(); + if (modelAPIConfig == null || CollUtil.isEmpty(modelAPIConfig.getRoutes())) { + log.error("Failed to build URI: model API config is null or contains no routes"); + return null; + } + + String completionPath = "/chat/completions"; + // Find matching route or use first route as fallback + HttpRouteResult route = + modelAPIConfig.getRoutes().stream() + .filter( + r -> + Optional.ofNullable(r.getMatch()) + .map(HttpRouteResult.RouteMatchResult::getPath) + .map(HttpRouteResult.RouteMatchPath::getValue) + .filter(path -> path.endsWith(completionPath)) + .isPresent()) + .findFirst() + .orElseGet(() -> modelAPIConfig.getRoutes().get(0)); + + // Get and process path + String path = + Optional.ofNullable(route.getMatch()) + .map(HttpRouteResult.RouteMatchResult::getPath) + .map(HttpRouteResult.RouteMatchPath::getValue) + .map( + p -> + p.endsWith(completionPath) + ? p.substring( + 0, p.length() - completionPath.length()) + : p) + .orElse(""); + + UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); + + // Try to get public domain first, fallback to first domain + DomainResult domain = + route.getDomains().stream() + .filter(d -> !StrUtil.equalsIgnoreCase(d.getNetworkType(), "intranet")) + .findFirst() + .orElseGet( + () -> + CollUtil.isNotEmpty(route.getDomains()) + ? route.getDomains().get(0) + : null); + + if (domain != null) { + String protocol = + StrUtil.isNotBlank(domain.getProtocol()) + ? domain.getProtocol().toLowerCase() + : "http"; + builder.scheme(protocol).host(domain.getDomain()); + if (domain.getPort() != null && domain.getPort() > 0) { + builder.port(domain.getPort()); + } + } else if (CollUtil.isNotEmpty(gatewayUris)) { + URI uri = gatewayUris.get(0); + builder.scheme(uri.getScheme() != null ? uri.getScheme() : "http").host(uri.getHost()); + if (uri.getPort() != -1) { + builder.port(uri.getPort()); + } + } else { + log.error("Failed to build URI: no valid domain found and no gateway URIs provided"); + return null; + } + + builder.path(path); + URI uri = builder.build().toUri(); + log.debug("Successfully built URI: {}", uri); + return uri; + } + + @Override + public List getProtocols() { + return Collections.singletonList(AIProtocol.OPENAI); + } +} diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/support/ChatBot.java b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/support/ChatBot.java new file mode 100644 index 000000000..0675896f0 --- /dev/null +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/support/ChatBot.java @@ -0,0 +1,86 @@ +package com.alibaba.himarket.service.hichat.support; + +import io.agentscope.core.ReActAgent; +import io.agentscope.core.agent.Event; +import io.agentscope.core.agent.EventType; +import io.agentscope.core.agent.StreamOptions; +import io.agentscope.core.memory.Memory; +import io.agentscope.core.message.Msg; +import java.util.List; +import java.util.Map; +import lombok.Builder; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Flux; + +@Slf4j +@Data +@Builder +public class ChatBot { + + private static final int MAX_MEMORY_SIZE = 30; + private static final long DEGRADED_TTL_MS = 2 * 60 * 1000; + + private final ReActAgent agent; + private final Map toolMetas; + + /** + * Whether this ChatBot is in degraded mode (some MCP tools failed to initialize) + */ + @Builder.Default private boolean degraded = false; + + /** + * Timestamp when this ChatBot was created + */ + @Builder.Default private long createTime = System.currentTimeMillis(); + + public Flux chat(Msg userMsg) { + // Truncate memory before adding new messages + truncateMemory(); + + StreamOptions streamOptions = + StreamOptions.builder() + .eventTypes(EventType.ALL) + .incremental(true) + .includeReasoningChunk(true) + // Exclude complete result to avoid duplication + .includeReasoningResult(true) + .build(); + + return agent.stream(userMsg, streamOptions); + } + + /** + * Truncate memory if it exceeds maximum size. Remove the oldest message. + */ + private void truncateMemory() { + Memory memory = agent.getMemory(); + List messages = memory.getMessages(); + + if (messages.size() > MAX_MEMORY_SIZE) { + // Remove the oldest message + messages.remove(0); + log.debug("Memory overflow, removed oldest message, current size: {}", messages.size()); + } + } + + /** + * Check if this ChatBot is still valid for use + * + *

Validation rules: + * - Normal ChatBot: always valid (no expiration) + * - Degraded ChatBot: valid only within 2 minutes after creation + * + * @return true if valid and can be reused, false if should be recreated + */ + public boolean isValid() { + // Degraded ChatBot: check TTL + if (degraded) { + long age = System.currentTimeMillis() - createTime; + return age <= DEGRADED_TTL_MS; + } + + // Normal ChatBot: always valid + return true; + } +} diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/support/ChatContext.java b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/support/ChatContext.java new file mode 100644 index 000000000..1e435f530 --- /dev/null +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/support/ChatContext.java @@ -0,0 +1,162 @@ +package com.alibaba.himarket.service.hichat.support; + +import com.alibaba.himarket.dto.result.chat.LlmInvokeResult; +import com.alibaba.himarket.support.chat.ChatUsage; +import java.util.Map; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Data +public class ChatContext { + + /** + * Chat ID for tracking + */ + private final String chatId; + + /** + * Answer content + */ + private final StringBuilder answerContent = new StringBuilder(); + + /** + * Chat usage (tokens) + */ + private ChatUsage usage; + + /** + * Success flag + */ + private boolean success = true; + + /** + * Request start time in milliseconds + */ + private Long startTime; + + /** + * First byte timeout (time to first byte in milliseconds) + */ + private Long firstByteTimeout; + + /** + * Tool name to tool metadata mapping + */ + private Map toolMetas; + + public ChatContext(String chatId) { + this.chatId = chatId; + } + + public void start() { + this.startTime = System.currentTimeMillis(); + } + + /** + * Record first byte arrival time + */ + public void recordFirstByteTimeout() { + if (firstByteTimeout == null && startTime != null) { + firstByteTimeout = System.currentTimeMillis() - startTime; + log.debug("First byte received after {} ms", firstByteTimeout); + } + } + + /** + * Stop timing and update usage with elapsed time + */ + public void stop() { + if (startTime == null) { + return; + } + + long elapsedTime = System.currentTimeMillis() - startTime; + + if (usage != null) { + usage.setElapsedTime(elapsedTime); + log.debug("Total elapsed time: {} ms", elapsedTime); + } + } + + /** + * Collect chat event and update context + * + * @param event ChatEvent to collect + */ + public void collect(ChatEvent event) { + if (event == null) { + return; + } + + switch (event.getType()) { + case ASSISTANT: + case THINKING: + // Accumulate assistant response and thinking content + if (event.getContent() != null) { + // Record first byte arrival time + recordFirstByteTimeout(); + answerContent.append(event.getContent()); + } + break; + + case DONE: + break; + + case ERROR: + // Mark as failed + this.success = false; + if (event.getMessage() != null) { + answerContent.append("\n[Error: ").append(event.getMessage()).append("]"); + } + break; + + default: + // Ignore other event types (TOOL_CALL, TOOL_RESULT, START) + break; + } + } + + /** + * Append additional content to answer + * + * @param content Content to append + */ + public void appendAnswer(String content) { + if (content != null) { + answerContent.append(content); + } + } + + /** + * Get complete answer content + * + * @return Complete answer as string + */ + public String getAnswer() { + return answerContent.toString(); + } + + /** + * Get tool metadata for a given tool name + * + * @param toolName tool name + * @return ToolMeta, or null if not found + */ + public ToolMeta getToolMeta(String toolName) { + return toolMetas != null ? toolMetas.get(toolName) : null; + } + + /** + * Convert to LlmInvokeResult for database persistence + * + * @return LlmInvokeResult instance + */ + public LlmInvokeResult toResult() { + return LlmInvokeResult.builder().success(success).answer(getAnswer()).usage(usage).build(); + } + + public void fail() { + this.success = false; + } +} diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/support/ChatEvent.java b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/support/ChatEvent.java new file mode 100644 index 000000000..e357cc174 --- /dev/null +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/support/ChatEvent.java @@ -0,0 +1,247 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.alibaba.himarket.service.hichat.support; + +import com.alibaba.himarket.support.chat.ChatUsage; +import lombok.Builder; +import lombok.Data; + +/** + * Unified streaming chunk structure for chat responses. + * + *

Structure: + *

+ * {
+ *   "chatId": "chat-id",
+ *   "type": "assistant|thinking|tool_call|tool_result|done|error",
+ *   "content": ...,  // main content (varies by type)
+ *   "usage": {...},  // token usage (optional)
+ *   "error": "...",  // error code (optional)
+ *   "message": "..." // error message (optional)
+ * }
+ * 
+ */ +@Data +@Builder +public class ChatEvent { + + /** + * Chat ID + */ + private String chatId; + + /** + * Chunk type + */ + private EventType type; + + /** + * Main content (type varies by chunk type) + */ + private Object content; + + /** + * Token usage statistics (optional, mainly for DONE type) + */ + private ChatUsage usage; + + /** + * Error code (only for ERROR type) + */ + private String error; + + /** + * Error message (only for ERROR type) + */ + private String message; + + /** + * Chunk type enumeration. + * + *

Serialized as lowercase with underscores in JSON (e.g., "assistant", "tool_call"). + */ + public enum EventType { + /** + * Stream started + */ + START, + + /** + * Assistant response + */ + ASSISTANT, + + /** + * Thinking/reasoning process + */ + THINKING, + + /** + * Tool call initiated + */ + TOOL_CALL, + + /** + * Tool execution result + */ + TOOL_RESULT, + + /** + * Stream completed + */ + DONE, + + /** + * Error occurred + */ + ERROR + } + + /** + * Create a start chunk (stream started) + * + * @param chatId Conversation ID + */ + public static ChatEvent start(String chatId) { + return ChatEvent.builder().chatId(chatId).type(EventType.START).build(); + } + + /** + * Create an assistant response chunk + * + * @param chatId Conversation ID + * @param text Assistant response text + */ + public static ChatEvent text(String chatId, String text) { + return ChatEvent.builder().chatId(chatId).type(EventType.ASSISTANT).content(text).build(); + } + + /** + * Create a thinking chunk + * + * @param chatId Conversation ID + * @param thought Thinking content + */ + public static ChatEvent thinking(String chatId, String thought) { + return ChatEvent.builder().chatId(chatId).type(EventType.THINKING).content(thought).build(); + } + + /** + * Create a tool call chunk + * + * @param chatId Conversation ID + * @param toolCall Tool call details + */ + public static ChatEvent toolCall(String chatId, ToolCallContent toolCall) { + return ChatEvent.builder() + .chatId(chatId) + .type(EventType.TOOL_CALL) + .content(toolCall) + .build(); + } + + /** + * Create a tool result chunk + * + * @param chatId Conversation ID + * @param toolResult Tool execution result + */ + public static ChatEvent toolResult(String chatId, ToolResultContent toolResult) { + return ChatEvent.builder() + .chatId(chatId) + .type(EventType.TOOL_RESULT) + .content(toolResult) + .build(); + } + + /** + * Create a done chunk (stream completed) + * + * @param chatId Conversation ID + * @param usage Token usage information + */ + public static ChatEvent done(String chatId, ChatUsage usage) { + return ChatEvent.builder().chatId(chatId).type(EventType.DONE).usage(usage).build(); + } + + /** + * Create an error chunk + * + * @param chatId Conversation ID + * @param code Error code + * @param errorMessage Error message + */ + public static ChatEvent error(String chatId, String code, String errorMessage) { + return ChatEvent.builder() + .chatId(chatId) + .type(EventType.ERROR) + .error(code) + .message(errorMessage) + .build(); + } + + /** + * Tool call content structure (used in content field when type=tool_call) + */ + @Data + @Builder + public static class ToolCallContent { + /** + * Tool call ID + */ + private String id; + + /** + * Tool name + */ + private String name; + + /** + * Tool arguments (as JSON object) + */ + private Object arguments; + + /** + * MCP server name (optional, identifies which MCP server provides this tool) + */ + private String mcpServerName; + } + + /** + * Tool result content structure (used in content field when type=tool_result) + */ + @Data + @Builder + public static class ToolResultContent { + /** + * Tool call ID (matches the call) + */ + private String id; + + /** + * Tool name + */ + private String name; + + /** + * Tool execution result + */ + private Object result; + } +} diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/support/ChatFormatter.java b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/support/ChatFormatter.java new file mode 100644 index 000000000..1d0e91fad --- /dev/null +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/support/ChatFormatter.java @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.alibaba.himarket.service.hichat.support; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.himarket.support.chat.ChatUsage; +import io.agentscope.core.agent.Event; +import io.agentscope.core.agent.EventType; +import io.agentscope.core.message.Msg; +import io.agentscope.core.message.ThinkingBlock; +import io.agentscope.core.message.ToolResultBlock; +import io.agentscope.core.message.ToolUseBlock; +import java.util.ArrayList; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Flux; + +/** + * Formats AgentScope events into HiChat events for frontend streaming. + * + *

Event flow: + *

+ * 1. REASONING (isLast: false) - streaming chunks (thinking, text, tool call fragments)
+ * 2. REASONING (isLast: true)  - final complete message (tool calls with parsed args)
+ * 3. TOOL_RESULT               - tool execution results
+ * 4. SUMMARY                   - max iterations reached (with usage)
+ * 5. AGENT_RESULT              - final result (with usage)
+ * 
+ */ +@Slf4j +public class ChatFormatter { + + public Flux format(Event event, ChatContext chatContext) { + try { + Msg msg = event.getMessage(); + EventType type = event.getType(); + + log.debug( + "Converting event - type: {}, isLast: {}, msg: {}", + type, + event.isLast(), + JSONUtil.toJsonStr(msg)); + + switch (type) { + case REASONING: + return handleReasoning(msg, event.isLast(), chatContext); + + case TOOL_RESULT: + return handleToolResult(msg, chatContext); + + case SUMMARY: + return handleSummary(msg, chatContext); + + case AGENT_RESULT: + return handleAgentResult(msg, chatContext); + + case HINT: + // Skip internal events (RAG context) + log.debug("Skipping HINT event (internal)"); + return Flux.empty(); + + default: + log.debug("Skipping unknown event type: {}", type); + return Flux.empty(); + } + + } catch (Exception e) { + log.error("Error converting event to ChatEvent", e); + return Flux.just( + ChatEvent.error(chatContext.getChatId(), "CONVERSION_ERROR", e.getMessage())); + } + } + + private Flux handleReasoning(Msg msg, boolean isLast, ChatContext chatContext) { + List chunks = new ArrayList<>(); + String chatId = chatContext.getChatId(); + + // 1. Extract thinking content + List thinkingBlocks = msg.getContentBlocks(ThinkingBlock.class); + for (ThinkingBlock thinking : thinkingBlocks) { + if (StrUtil.isNotBlank(thinking.getThinking())) { + chunks.add(ChatEvent.thinking(chatId, thinking.getThinking())); + } + } + + // 2. Extract text content (model's response) + String textContent = msg.getTextContent(); + if (StrUtil.isNotBlank(textContent)) { + if (isLast) { + getUsage(msg, chatContext); + // Skip final complete text to avoid duplication + log.debug( + "Skipping final complete text (isLast=true, length={})", + textContent.length()); + } else { + // Send incremental chunks for streaming + chunks.add(ChatEvent.text(chatId, textContent)); + } + } + + // 3. Extract tool calls (only send when isLast=true) + // + // Streaming phase (isLast=false): + // - input is EMPTY {}, parameters are in content as JSON fragments + // - Skip: no useful data to send + // + // Final phase (isLast=true): + // - input contains COMPLETE PARSED arguments (accumulated by AgentScope) + // - Send: complete tool call with full parameters + // + List toolUseBlocks = msg.getContentBlocks(ToolUseBlock.class); + if (!toolUseBlocks.isEmpty()) { + if (isLast) { + // Final complete tool calls with parsed input arguments + log.debug("Sending {} complete tool call(s)", toolUseBlocks.size()); + for (ToolUseBlock toolUse : toolUseBlocks) { + ToolMeta toolMeta = chatContext.getToolMeta(toolUse.getName()); + String mcpServerName = toolMeta != null ? toolMeta.getMcpServerName() : null; + + ChatEvent.ToolCallContent tc = + ChatEvent.ToolCallContent.builder() + .id(toolUse.getId()) + .name(toolUse.getName()) + .arguments(toolUse.getInput()) + .mcpServerName(mcpServerName) + .build(); + chunks.add(ChatEvent.toolCall(chatId, tc)); + } + } else { + // Skip streaming tool call chunks (input is empty, contains fragments) + log.debug("Skipping {} streaming tool call chunk(s)", toolUseBlocks.size()); + } + } + + return Flux.fromIterable(chunks); + } + + private Flux handleToolResult(Msg msg, ChatContext chatContext) { + List chunks = new ArrayList<>(); + String chatId = chatContext.getChatId(); + + // Extract and send all tool execution results + List toolResults = msg.getContentBlocks(ToolResultBlock.class); + for (ToolResultBlock toolResult : toolResults) { + ChatEvent.ToolResultContent tr = + ChatEvent.ToolResultContent.builder() + .id(toolResult.getId()) + .name(toolResult.getName()) + .result(toolResult.getOutput()) + .build(); + chunks.add(ChatEvent.toolResult(chatId, tr)); + } + + return Flux.fromIterable(chunks); + } + + private Flux handleSummary(Msg msg, ChatContext chatContext) { + // Get usage from SUMMARY (emitted when max iterations reached) + getUsage(msg, chatContext); + + // Send summary text if available + if (msg != null && StrUtil.isNotBlank(msg.getTextContent())) { + return Flux.just(ChatEvent.text(chatContext.getChatId(), msg.getTextContent())); + } + return Flux.empty(); + } + + private Flux handleAgentResult(Msg msg, ChatContext chatContext) { + // Get usage from AGENT_RESULT (contains final complete result and usage) + getUsage(msg, chatContext); + + // Skip this event - complete content already sent via streaming chunks + log.debug("Skipping AGENT_RESULT event (usage captured)"); + return Flux.empty(); + } + + /** + * Get usage information from message and saves to context. + * + *

Usage is only available in certain events: + *

    + *
  • SUMMARY - when max iterations reached
  • + *
  • AGENT_RESULT - final complete result
  • + *
+ * + * @param msg message containing usage info + * @param chatContext context to save usage to + */ + private void getUsage(Msg msg, ChatContext chatContext) { + if (msg == null || msg.getChatUsage() == null) { + return; + } + + // Only capture once (first occurrence wins) + if (chatContext.getUsage() != null) { + return; + } + + io.agentscope.core.model.ChatUsage chatUsage = msg.getChatUsage(); + ChatUsage usage = + ChatUsage.builder() + .inputTokens(chatUsage.getInputTokens()) + .outputTokens(chatUsage.getOutputTokens()) + .totalTokens(chatUsage.getTotalTokens()) + .firstByteTimeout(chatContext.getFirstByteTimeout()) + // elapsedTime will be set by ChatContext.stop() + .build(); + + chatContext.setUsage(usage); + log.debug( + "Get usage: input={}, output={}, total={}", + usage.getInputTokens(), + usage.getOutputTokens(), + usage.getTotalTokens()); + } +} diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/support/InvokeModelParam.java b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/support/InvokeModelParam.java new file mode 100644 index 000000000..21b584487 --- /dev/null +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/support/InvokeModelParam.java @@ -0,0 +1,59 @@ +package com.alibaba.himarket.service.hichat.support; + +import com.alibaba.himarket.dto.result.consumer.CredentialContext; +import com.alibaba.himarket.dto.result.product.ProductResult; +import com.alibaba.himarket.support.chat.mcp.MCPTransportConfig; +import io.agentscope.core.message.Msg; +import java.util.List; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class InvokeModelParam { + + /** + * Chat ID + */ + private String chatId; + + /** + * Session ID + */ + private String sessionId; + + /** + * Model Product + */ + private ProductResult product; + + /** + * User message, contains user question and multimodal + */ + private Msg userMessage; + + /** + * History messages for initializing memory + */ + private List historyMessages; + + /** + * If need web search + */ + private Boolean enableWebSearch; + + /** + * Gateway ID + */ + private String gatewayId; + + /** + * MCP servers with transport config + */ + private List mcpConfigs; + + /** + * Credential for invoking the Model and MCP + */ + private CredentialContext credentialContext; +} diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/support/LlmChatRequest.java b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/support/LlmChatRequest.java new file mode 100644 index 000000000..fcd4e418e --- /dev/null +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/support/LlmChatRequest.java @@ -0,0 +1,112 @@ +package com.alibaba.himarket.service.hichat.support; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.himarket.dto.result.product.ProductResult; +import com.alibaba.himarket.support.chat.mcp.MCPTransportConfig; +import io.agentscope.core.message.Msg; +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.Builder; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +@Builder +@Slf4j +@Data +public class LlmChatRequest { + + /** + * The unique chatId + */ + private String chatId; + + /** + * Session ID + */ + private String sessionId; + + /** + * Model product + */ + private ProductResult product; + + /** + * User message + */ + private Msg userMessages; + + /** + * History messages for initializing memory + */ + private List historyMessages; + + /** + * URI, use this uri to request model + */ + private URI uri; + + /** + * API key + */ + private String apiKey; + + /** + * Custom headers + */ + private Map headers; + + /** + * Custom query parameters + */ + private Map queryParams; + + /** + * Custom json body + */ + private Map bodyParams; + + /** + * If not empty, use these URIs to resolve DNS + */ + private List gatewayUris; + + /** + * MCP servers with transport config + */ + private List mcpConfigs; + + @Deprecated + public void tryResolveDns() { + if (CollUtil.isEmpty(gatewayUris) || !"http".equalsIgnoreCase(uri.getScheme())) { + return; + } + + try { + // Randomly select a gateway URI + URI gatewayUri = gatewayUris.get(0); + + String originalHost = uri.getHost(); + // Build new URI keeping original path and query but replacing scheme, host and port + this.uri = + new URI( + gatewayUri.getScheme(), + uri.getUserInfo(), + gatewayUri.getHost(), + gatewayUri.getPort(), + uri.getPath(), + uri.getQuery(), + uri.getFragment()); + + if (this.headers == null) { + this.headers = new HashMap<>(); + } + // Set Host header + this.headers.put("Host", originalHost); + + } catch (Exception e) { + log.warn("Failed to resolve DNS for URI: {}", uri, e); + } + } +} diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/support/ToolMeta.java b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/support/ToolMeta.java new file mode 100644 index 000000000..73ddf27e0 --- /dev/null +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/hichat/support/ToolMeta.java @@ -0,0 +1,21 @@ +package com.alibaba.himarket.service.hichat.support; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ToolMeta { + + /** + * MCP server name + */ + private String mcpServerName; + + /** + * Tool name + */ + private String toolName; + + // Add other fields as needed +} diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/ChatSessionServiceImpl.java b/himarket-server/src/main/java/com/alibaba/himarket/service/impl/ChatSessionServiceImpl.java index 50a97266e..b6d56bd32 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/ChatSessionServiceImpl.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/impl/ChatSessionServiceImpl.java @@ -20,6 +20,7 @@ package com.alibaba.himarket.service.impl; import cn.hutool.core.collection.CollUtil; +import cn.hutool.extra.spring.SpringUtil; import com.alibaba.himarket.core.constant.Resources; import com.alibaba.himarket.core.event.ChatSessionDeletingEvent; import com.alibaba.himarket.core.exception.BusinessException; @@ -42,7 +43,6 @@ import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -63,9 +63,9 @@ public class ChatSessionServiceImpl implements ChatSessionService { private final ContextHolder contextHolder; - private final ApplicationEventPublisher eventPublisher; - - /** Allowed number of sessions per user */ + /** + * Allowed number of sessions per user + */ private static final int MAX_SESSIONS_PER_USER = 20; @Override @@ -146,7 +146,7 @@ public ChatSessionResult updateSession(String sessionId, UpdateChatSessionParam public void deleteSession(String sessionId) { ChatSession session = findUserSession(sessionId); - eventPublisher.publishEvent(new ChatSessionDeletingEvent(sessionId)); + SpringUtil.getApplicationContext().publishEvent(new ChatSessionDeletingEvent(sessionId)); sessionRepository.delete(session); } @@ -156,7 +156,9 @@ public void deleteSession(String sessionId) { // sessionRepository.saveAndFlush(session); // } - /** Clean up extra sessions */ + /** + * Clean up extra sessions + */ private void cleanupExtraSessions() { long count = sessionRepository.countByUserId(contextHolder.getUser()); if (count > MAX_SESSIONS_PER_USER) { diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/ConsumerServiceImpl.java b/himarket-server/src/main/java/com/alibaba/himarket/service/impl/ConsumerServiceImpl.java index 9dc0f2fc5..1e74ce500 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/ConsumerServiceImpl.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/impl/ConsumerServiceImpl.java @@ -414,6 +414,15 @@ public PageResult listSubscriptions( }); } + @Override + public List listConsumerSubscriptions(String consumerId) { + List subscriptions = + subscriptionRepository.findAllByConsumerId(consumerId); + return subscriptions.stream() + .map(subscription -> new SubscriptionResult().convertFrom(subscription)) + .collect(Collectors.toList()); + } + @Override public SubscriptionResult approveSubscription(String consumerId, String subscriptionId) { existsConsumer(consumerId); @@ -648,7 +657,7 @@ private boolean isConsumerExistsInGateway(String gwConsumerId, GatewayConfig gat @EventListener @Async("taskExecutor") - public void handleDeveloperDeletion(DeveloperDeletingEvent event) { + public void onDeveloperDeletion(DeveloperDeletingEvent event) { String developerId = event.getDeveloperId(); log.info("Cleaning consumers for developer {}", developerId); @@ -665,7 +674,7 @@ public void handleDeveloperDeletion(DeveloperDeletingEvent event) { @EventListener @Async("taskExecutor") - public void handleProductDeletion(ProductDeletingEvent event) { + public void onProductDeletion(ProductDeletingEvent event) { String productId = event.getProductId(); log.info("Cleaning subscriptions for product {}", productId); diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/DeveloperServiceImpl.java b/himarket-server/src/main/java/com/alibaba/himarket/service/impl/DeveloperServiceImpl.java index 787e00eae..423d617f6 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/DeveloperServiceImpl.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/impl/DeveloperServiceImpl.java @@ -21,6 +21,7 @@ import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; import com.alibaba.himarket.core.constant.Resources; import com.alibaba.himarket.core.event.DeveloperDeletingEvent; import com.alibaba.himarket.core.event.PortalDeletingEvent; @@ -54,7 +55,6 @@ import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -77,8 +77,6 @@ public class DeveloperServiceImpl implements DeveloperService { private final ContextHolder contextHolder; - private final ApplicationEventPublisher eventPublisher; - private final ConsumerService consumerService; @Override @@ -215,7 +213,7 @@ private String buildExternalName(String provider, String subject) { @Override public void deleteDeveloper(String developerId) { - eventPublisher.publishEvent(new DeveloperDeletingEvent(developerId)); + SpringUtil.getApplicationContext().publishEvent(new DeveloperDeletingEvent(developerId)); externalRepository.deleteByDeveloper_DeveloperId(developerId); developerRepository.findByDeveloperId(developerId).ifPresent(developerRepository::delete); } @@ -280,7 +278,7 @@ public void updateProfile(UpdateDeveloperParam param) { @EventListener @Async("taskExecutor") - public void handlePortalDeletion(PortalDeletingEvent event) { + public void onPortalDeletion(PortalDeletingEvent event) { String portalId = event.getPortalId(); List developers = developerRepository.findByPortalId(portalId); developers.forEach(developer -> deleteDeveloper(developer.getDeveloperId())); diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/GatewayServiceImpl.java b/himarket-server/src/main/java/com/alibaba/himarket/service/impl/GatewayServiceImpl.java index 643147d7b..a106b21b2 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/GatewayServiceImpl.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/impl/GatewayServiceImpl.java @@ -48,6 +48,7 @@ import jakarta.persistence.criteria.Predicate; import java.net.URI; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -359,7 +360,11 @@ private GatewayOperator getOperator(Gateway gateway) { @Override public List fetchGatewayUris(String gatewayId) { Gateway gateway = findGateway(gatewayId); - return getOperator(gateway).fetchGatewayUris(gateway); + List gatewayUris = getOperator(gateway).fetchGatewayUris(gateway); + + // Shuffle the list + Collections.shuffle(gatewayUris); + return gatewayUris; } private Specification buildGatewaySpec(QueryGatewayParam param) { diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/IdpServiceImpl.java b/himarket-server/src/main/java/com/alibaba/himarket/service/impl/IdpServiceImpl.java index dcd5ca1a2..03f64a50b 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/IdpServiceImpl.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/impl/IdpServiceImpl.java @@ -27,6 +27,7 @@ import com.alibaba.himarket.core.exception.BusinessException; import com.alibaba.himarket.core.exception.ErrorCode; import com.alibaba.himarket.service.IdpService; +import com.alibaba.himarket.service.gateway.factory.HTTPClientFactory; import com.alibaba.himarket.support.enums.GrantType; import com.alibaba.himarket.support.enums.PublicKeyFormat; import com.alibaba.himarket.support.portal.*; @@ -48,7 +49,7 @@ @Slf4j public class IdpServiceImpl implements IdpService { - private final RestTemplate restTemplate; + private final RestTemplate restTemplate = HTTPClientFactory.createRestTemplate(); @Override public void validateOidcConfigs(List oidcConfigs) { diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/OidcServiceImpl.java b/himarket-server/src/main/java/com/alibaba/himarket/service/impl/OidcServiceImpl.java index a69a64b34..9a01829a2 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/OidcServiceImpl.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/impl/OidcServiceImpl.java @@ -42,6 +42,7 @@ import com.alibaba.himarket.service.DeveloperService; import com.alibaba.himarket.service.OidcService; import com.alibaba.himarket.service.PortalService; +import com.alibaba.himarket.service.gateway.factory.HTTPClientFactory; import com.alibaba.himarket.support.enums.DeveloperAuthType; import com.alibaba.himarket.support.enums.GrantType; import com.alibaba.himarket.support.portal.AuthCodeConfig; @@ -75,7 +76,7 @@ public class OidcServiceImpl implements OidcService { private final DeveloperService developerService; - private final RestTemplate restTemplate; + private final RestTemplate restTemplate = HTTPClientFactory.createRestTemplate(); private final ContextHolder contextHolder; diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/PortalServiceImpl.java b/himarket-server/src/main/java/com/alibaba/himarket/service/impl/PortalServiceImpl.java index dca008df2..02cb8487c 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/PortalServiceImpl.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/impl/PortalServiceImpl.java @@ -22,6 +22,7 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; import com.alibaba.himarket.core.constant.Resources; import com.alibaba.himarket.core.event.PortalDeletingEvent; import com.alibaba.himarket.core.exception.BusinessException; @@ -63,7 +64,6 @@ import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; @@ -79,8 +79,6 @@ public class PortalServiceImpl implements PortalService { private final PortalDomainRepository portalDomainRepository; - private final ApplicationEventPublisher eventPublisher; - private final SubscriptionRepository subscriptionRepository; private final ContextHolder contextHolder; @@ -100,7 +98,9 @@ public PortalResult createPortal(CreatePortalParam param) { portal -> { throw new BusinessException( ErrorCode.CONFLICT, - StrUtil.format("{}:{}已存在", Resources.PORTAL, portal.getName())); + StrUtil.format( + "Portal with name `{}` already exists", + portal.getName())); }); String portalId = IdGenerator.genPortalId(); @@ -146,7 +146,7 @@ public void existsPortal(String portalId) { public PageResult listPortals(Pageable pageable) { Page portals = portalRepository.findAll(pageable); - // 填充Domain + // Fill domain if (portals.hasContent()) { List portalIds = portals.getContent().stream() @@ -182,30 +182,31 @@ public PortalResult updatePortal(String portalId, UpdatePortalParam param) { p -> { throw new BusinessException( ErrorCode.CONFLICT, - StrUtil.format("{}:{}已存在", Resources.PORTAL, portal.getName())); + StrUtil.format( + "Portal with name `{}` already exists", + portal.getName())); }); param.update(portal); - // 验证配置 PortalSettingConfig setting = portal.getPortalSettingConfig(); - // 验证OIDC配置 + // Verify OIDC configs if (CollUtil.isNotEmpty(setting.getOidcConfigs())) { idpService.validateOidcConfigs(setting.getOidcConfigs()); } - // 验证OAuth2配置 + // Verify OAuth2 configs if (CollUtil.isNotEmpty(setting.getOauth2Configs())) { idpService.validateOAuth2Configs(setting.getOauth2Configs()); } - // 验证搜索引擎配置(新增) + // Verify search engine config if (setting.getSearchEngineConfig() != null) { validateSearchEngineConfig(setting.getSearchEngineConfig()); } - // 至少保留一种认证方式 + // At least keep one authentication method if (BooleanUtil.isFalse(setting.getBuiltinAuthEnabled())) { boolean enabledOidc = Optional.ofNullable(setting.getOidcConfigs()) @@ -214,7 +215,9 @@ public PortalResult updatePortal(String portalId, UpdatePortalParam param) { .orElse(false); if (!enabledOidc) { - throw new BusinessException(ErrorCode.INVALID_REQUEST, "至少配置一种认证方式"); + throw new BusinessException( + ErrorCode.INVALID_REQUEST, + "At least one authentication method must be configured"); } } portalRepository.saveAndFlush(portal); @@ -226,11 +229,11 @@ public PortalResult updatePortal(String portalId, UpdatePortalParam param) { public void deletePortal(String portalId) { Portal portal = findPortal(portalId); - // 清理Domain + // Clean up domains portalDomainRepository.deleteAllByPortalId(portalId); - // 异步清理门户资源 - eventPublisher.publishEvent(new PortalDeletingEvent(portalId)); + // Asynchronously clean up portal resources + SpringUtil.getApplicationContext().publishEvent(new PortalDeletingEvent(portalId)); portalRepository.delete(portal); } @@ -252,9 +255,8 @@ public PortalResult bindDomain(String portalId, BindDomainParam param) { throw new BusinessException( ErrorCode.CONFLICT, StrUtil.format( - "{}:{}已存在", - Resources.PORTAL_DOMAIN, - portalDomain.getDomain())); + "Portal domain `{}` already exists", + param.getDomain())); }); PortalDomain portalDomain = param.convertTo(); @@ -270,9 +272,11 @@ public PortalResult unbindDomain(String portalId, String domain) { .findByPortalIdAndDomain(portalId, domain) .ifPresent( portalDomain -> { - // 默认域名不允许解绑 + // Default domain cannot be unbound if (portalDomain.getType() == DomainType.DEFAULT) { - throw new BusinessException(ErrorCode.INVALID_REQUEST, "默认域名不允许解绑"); + throw new BusinessException( + ErrorCode.INVALID_REQUEST, + "Default domain cannot be unbound"); } portalDomainRepository.delete(portalDomain); }); @@ -365,9 +369,10 @@ private Portal findPortal(String portalId) { ErrorCode.NOT_FOUND, Resources.PORTAL, portalId)); } - // ========== 搜索引擎配置查询实现 ========== - - /** 核心方法:根据引擎类型获取 API Key 供 TalkSearchAbilityServiceGoogleImpl 等搜索能力调用 */ + /** + * Core method: Get API Key based on engine type for search ability calls + * (e.g. TalkSearchAbilityServiceGoogleImpl) + */ @Override public String getSearchEngineApiKey(String portalId, SearchEngineType engineType) { Portal portal = findPortal(portalId); @@ -375,29 +380,32 @@ public String getSearchEngineApiKey(String portalId, SearchEngineType engineType if (settings == null || settings.getSearchEngineConfig() == null) { throw new BusinessException( - ErrorCode.NOT_FOUND, StrUtil.format("Portal {} 未配置搜索引擎", portalId)); + ErrorCode.NOT_FOUND, + StrUtil.format("Portal {} has not configured search engine", portalId)); } SearchEngineConfig config = settings.getSearchEngineConfig(); - // 检查引擎类型是否匹配 + // Check if engine type matches if (config.getEngineType() != engineType) { throw new BusinessException( ErrorCode.NOT_FOUND, StrUtil.format( - "Portal {} 配置的搜索引擎类型是 {},不是 {}", + "Portal {} configured search engine type is {}, not {}", portalId, config.getEngineType(), engineType)); } - // 检查是否启用 + // Check if enabled if (!config.isEnabled()) { throw new BusinessException( - ErrorCode.INVALID_REQUEST, StrUtil.format("Portal {} 的搜索引擎已禁用", portalId)); + ErrorCode.INVALID_REQUEST, + StrUtil.format("Search engine for Portal {} is disabled", portalId)); } - return config.getApiKey(); // API Key 会自动解密(通过 @Encrypted 注解) + return config + .getApiKey(); // API Key will be automatically decrypted (via @Encrypted annotation) } @Override @@ -412,35 +420,34 @@ public SearchEngineConfig getSearchEngineConfig(String portalId) { return settings.getSearchEngineConfig(); } - // ========== 私有辅助方法 ========== - - /** 验证搜索引擎配置 */ private void validateSearchEngineConfig(SearchEngineConfig config) { if (config == null) { return; } - // 验证引擎类型是否支持 + // Validate if engine type is supported if (config.getEngineType() == null) { - throw new BusinessException(ErrorCode.INVALID_REQUEST, "搜索引擎类型不能为空"); + throw new BusinessException( + ErrorCode.INVALID_REQUEST, "Search engine type cannot be empty"); } if (!SearchEngineType.isSupported(config.getEngineType())) { throw new BusinessException( ErrorCode.INVALID_REQUEST, StrUtil.format( - "不支持的搜索引擎类型: {},当前仅支持: {}", + "Unsupported search engine type: {}, currently only supports: {}", config.getEngineType(), SearchEngineType.getSupportedTypes())); } - // 验证必填字段 + // Validate required fields if (StrUtil.isBlank(config.getEngineName())) { - throw new BusinessException(ErrorCode.INVALID_REQUEST, "搜索引擎名称不能为空"); + throw new BusinessException( + ErrorCode.INVALID_REQUEST, "Search engine name cannot be empty"); } if (StrUtil.isBlank(config.getApiKey())) { - throw new BusinessException(ErrorCode.INVALID_REQUEST, "API Key不能为空"); + throw new BusinessException(ErrorCode.INVALID_REQUEST, "API Key cannot be empty"); } log.info( diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/ProductServiceImpl.java b/himarket-server/src/main/java/com/alibaba/himarket/service/impl/ProductServiceImpl.java index 30f6d1d8c..ef05b3e9b 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/ProductServiceImpl.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/impl/ProductServiceImpl.java @@ -22,6 +22,7 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; import cn.hutool.json.JSONUtil; import com.alibaba.himarket.core.constant.Resources; import com.alibaba.himarket.core.event.PortalDeletingEvent; @@ -49,23 +50,22 @@ import com.alibaba.himarket.entity.*; import com.alibaba.himarket.repository.*; import com.alibaba.himarket.service.*; +import com.alibaba.himarket.service.hichat.manager.ToolManager; import com.alibaba.himarket.support.chat.mcp.MCPTransportConfig; import com.alibaba.himarket.support.enums.ProductStatus; import com.alibaba.himarket.support.enums.ProductType; import com.alibaba.himarket.support.enums.SourceType; import com.alibaba.himarket.support.product.NacosRefConfig; import com.github.benmanes.caffeine.cache.Cache; +import io.agentscope.core.tool.mcp.McpClientWrapper; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; import jakarta.persistence.criteria.Subquery; import jakarta.transaction.Transactional; -import java.io.IOException; import java.util.*; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -79,8 +79,6 @@ @Transactional public class ProductServiceImpl implements ProductService { - private final ApplicationContext applicationContext; - private final ContextHolder contextHolder; private final PortalService portalService; @@ -99,11 +97,13 @@ public class ProductServiceImpl implements ProductService { private final NacosService nacosService; - private final ApplicationEventPublisher eventPublisher; - private final ProductCategoryService productCategoryService; - /** Cache to prevent duplicate sync within interval (5 minutes default) */ + private final ToolManager toolManager; + + /** + * Cache to prevent duplicate sync within interval (5 minutes default) + */ private final Cache productSyncCache = CacheUtil.newCache(5); @Override @@ -147,8 +147,8 @@ public ProductResult getProduct(String productId) { .ifPresent( o -> { productSyncCache.put(productId, Boolean.TRUE); - eventPublisher.publishEvent( - new ProductConfigReloadEvent(productId)); + SpringUtil.getApplicationContext() + .publishEvent(new ProductConfigReloadEvent(productId)); }); } @@ -172,7 +172,6 @@ public PageResult listProducts(QueryProductParam param, Pageable product -> { ProductResult result = new ProductResult().convertFrom(product); fillProduct(result); - fillProductSubscribeInfo(result, param); return result; }); } @@ -309,7 +308,7 @@ public void deleteProduct(String productId) { productRefRepository.deleteByProductId(productId); // Asynchronously clean up product resources - eventPublisher.publishEvent(new ProductDeletingEvent(productId)); + SpringUtil.getApplicationContext().publishEvent(new ProductDeletingEvent(productId)); } private Product findProduct(String productId) { @@ -379,8 +378,7 @@ public void deleteProductRef(String productId) { @EventListener @Async("taskExecutor") - @Override - public void handlePortalDeletion(PortalDeletingEvent event) { + public void onPortalDeletion(PortalDeletingEvent event) { String portalId = event.getPortalId(); try { publicationRepository.deleteAllByPortalId(portalId); @@ -509,38 +507,36 @@ public McpToolListResult listMcpTools(String productId) { ErrorCode.INVALID_REQUEST, "API product is not a mcp server"); } - ConsumerService consumerService = applicationContext.getBean(ConsumerService.class); + ConsumerService consumerService = + SpringUtil.getApplicationContext().getBean(ConsumerService.class); String consumerId = consumerService.getPrimaryConsumer().getConsumerId(); - - boolean subscribed = - subscriptionRepository - .findByConsumerIdAndProductId(consumerId, productId) - .isPresent(); - if (!subscribed) { - throw new BusinessException( - ErrorCode.INVALID_REQUEST, - "API product is not subscribed, not allowed to list tools"); - } + // Check subscription status + subscriptionRepository + .findByConsumerIdAndProductId(consumerId, productId) + .orElseThrow( + () -> + new BusinessException( + ErrorCode.INVALID_REQUEST, + "API product is not subscribed, not allowed to list" + + " tools")); // Initialize client and fetch tools MCPTransportConfig transportConfig = product.getMcpConfig().toTransportConfig(); CredentialContext credentialContext = consumerService.getDefaultCredential(contextHolder.getUser()); - McpToolListResult result = new McpToolListResult(); + transportConfig.setHeaders(credentialContext.copyHeaders()); + transportConfig.setQueryParams(credentialContext.copyQueryParams()); - try (McpClientWrapper mcpClientWrapper = - McpClientFactory.newClient(transportConfig, credentialContext)) { - if (mcpClientWrapper == null) { - throw new BusinessException( - ErrorCode.INTERNAL_ERROR, "Failed to initialize MCP client"); - } + McpToolListResult result = new McpToolListResult(); - result.setTools(mcpClientWrapper.listTools()); - return result; - } catch (IOException e) { - log.error("List mcp tools failed", e); - return result; + McpClientWrapper mcpClientWrapper = toolManager.getOrCreateClient(transportConfig); + if (mcpClientWrapper == null) { + throw new BusinessException( + ErrorCode.INTERNAL_ERROR, "Failed to initialize MCP client"); } + + result.setTools(mcpClientWrapper.listTools().block()); + return result; } private void syncConfig(Product product, ProductRef productRef) { @@ -784,7 +780,8 @@ private void fillProductSubscribeInfo(ProductResult product, QueryProductParam p } // Get default consumer id (use applicationContext to get bean to avoid circular dependency) - ConsumerService consumerService = applicationContext.getBean(ConsumerService.class); + ConsumerService consumerService = + SpringUtil.getApplicationContext().getBean(ConsumerService.class); String consumerId = consumerService.getPrimaryConsumer().getConsumerId(); // Check if product is subscribed by consumer @@ -797,7 +794,7 @@ private void fillProductSubscribeInfo(ProductResult product, QueryProductParam p @EventListener @Async("taskExecutor") - public void handleProductRefSync(ProductConfigReloadEvent event) { + public void onProductConfigReload(ProductConfigReloadEvent event) { String productId = event.getProductId(); try { diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/SearchRewirteServiceImpl.java b/himarket-server/src/main/java/com/alibaba/himarket/service/impl/SearchRewirteServiceImpl.java index 513e60723..27cf41eef 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/SearchRewirteServiceImpl.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/impl/SearchRewirteServiceImpl.java @@ -26,14 +26,14 @@ import com.alibaba.himarket.core.exception.ErrorCode; import com.alibaba.himarket.core.utils.CacheUtil; import com.alibaba.himarket.dto.params.chat.CreateChatParam; -import com.alibaba.himarket.dto.params.chat.InvokeModelParam; import com.alibaba.himarket.dto.result.chat.LlmInvokeResult; import com.alibaba.himarket.dto.result.product.ProductRefResult; import com.alibaba.himarket.dto.result.product.ProductResult; import com.alibaba.himarket.service.GatewayService; -import com.alibaba.himarket.service.LlmService; import com.alibaba.himarket.service.ProductService; import com.alibaba.himarket.service.SearchRewriteService; +import com.alibaba.himarket.service.legacy.InvokeModelParamLegacy; +import com.alibaba.himarket.service.legacy.LlmServiceLegacy; import com.alibaba.himarket.support.chat.ChatMessage; import com.alibaba.himarket.support.chat.search.SearchInput; import com.alibaba.himarket.support.enums.ChatRole; @@ -91,7 +91,7 @@ public class SearchRewirteServiceImpl implements SearchRewriteService { + "%s\n" + ""; - private final LlmService llmService; + private final LlmServiceLegacy llmServiceLegacy; private final ProductService productService; @@ -100,8 +100,10 @@ public class SearchRewirteServiceImpl implements SearchRewriteService { private final Cache> cache = CacheUtil.newCache(5); public SearchRewirteServiceImpl( - LlmService llmService, ProductService productService, GatewayService gatewayService) { - this.llmService = llmService; + LlmServiceLegacy llmServiceLegacy, + ProductService productService, + GatewayService gatewayService) { + this.llmServiceLegacy = llmServiceLegacy; this.productService = productService; this.gatewayService = gatewayService; } @@ -131,7 +133,7 @@ public SearchInput rewriteWithRetry(List chatMessages, CreateChatPa private SearchInput rewrite(List chatMessages, CreateChatParam param) { List messages = buildRewriteMessagesFromHistory(chatMessages); - InvokeModelParam invokeModelParam = buildInvokeModelParam(messages, param); + InvokeModelParamLegacy invokeModelParamLegacy = buildInvokeModelParam(messages, param); // 使用 CountDownLatch 等待异步调用完成 CountDownLatch latch = new CountDownLatch(1); @@ -142,8 +144,8 @@ private SearchInput rewrite(List chatMessages, CreateChatParam para MockHttpServletResponse mockResponse = new MockHttpServletResponse(); // 调用 LLM 进行查询重写 - llmService.invokeLLM( - invokeModelParam, + llmServiceLegacy.invokeLLM( + invokeModelParamLegacy, mockResponse, result -> { resultRef.set(result); @@ -226,7 +228,7 @@ private SearchInput parseSearchInput(String answer) { } } - private InvokeModelParam buildInvokeModelParam( + private InvokeModelParamLegacy buildInvokeModelParam( List messages, CreateChatParam param) { ProductResult productResult = productService.getProduct(param.getProductId()); @@ -235,7 +237,7 @@ private InvokeModelParam buildInvokeModelParam( String gatewayId = productRef.getGatewayId(); List gatewayUris = cache.get(gatewayId, gatewayService::fetchGatewayUris); - return InvokeModelParam.builder() + return InvokeModelParamLegacy.builder() .product(productResult) .chatMessages(messages) .gatewayUris(gatewayUris) diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/TalkSearchAbilityServiceGoogleImpl.java b/himarket-server/src/main/java/com/alibaba/himarket/service/impl/TalkSearchAbilityServiceGoogleImpl.java index 9e90a3d34..04f66c17f 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/TalkSearchAbilityServiceGoogleImpl.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/impl/TalkSearchAbilityServiceGoogleImpl.java @@ -24,6 +24,7 @@ import com.alibaba.himarket.core.security.ContextHolder; import com.alibaba.himarket.service.PortalService; import com.alibaba.himarket.service.TalkSearchAbilityService; +import com.alibaba.himarket.service.gateway.factory.HTTPClientFactory; import com.alibaba.himarket.support.chat.search.SearchContext; import com.alibaba.himarket.support.chat.search.SearchInput; import com.alibaba.himarket.support.enums.SearchEngineType; @@ -36,6 +37,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -48,10 +50,11 @@ @Service @Slf4j +@RequiredArgsConstructor public class TalkSearchAbilityServiceGoogleImpl implements TalkSearchAbilityService, ResponseEntity> { - private final RestTemplate restTemplate; + private final RestTemplate restTemplate = HTTPClientFactory.createRestTemplate(); private final PortalService portalService; private final ContextHolder contextHolder; @@ -81,13 +84,6 @@ public class TalkSearchAbilityServiceGoogleImpl "serpapi_knowledge_graph_search_link", "website"); - public TalkSearchAbilityServiceGoogleImpl( - RestTemplate restTemplate, PortalService portalService, ContextHolder contextHolder) { - this.restTemplate = restTemplate; - this.portalService = portalService; - this.contextHolder = contextHolder; - } - /** * 获取 Google 搜索 API Key 从当前 Portal 的配置中动态获取(自动解密) * diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/AbstractLlmService.java b/himarket-server/src/main/java/com/alibaba/himarket/service/legacy/AbstractLlmServiceLegacy.java similarity index 95% rename from himarket-server/src/main/java/com/alibaba/himarket/service/impl/AbstractLlmService.java rename to himarket-server/src/main/java/com/alibaba/himarket/service/legacy/AbstractLlmServiceLegacy.java index 396fcb28c..e876651e2 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/AbstractLlmService.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/legacy/AbstractLlmServiceLegacy.java @@ -17,7 +17,7 @@ * under the License. */ -package com.alibaba.himarket.service.impl; +package com.alibaba.himarket.service.legacy; import static com.alibaba.himarket.dto.result.chat.ChatAnswerMessage.MessageType; import static com.alibaba.himarket.dto.result.chat.ChatAnswerMessage.MessageType.*; @@ -29,17 +29,14 @@ import cn.hutool.json.JSONUtil; import com.alibaba.himarket.core.exception.ChatError; import com.alibaba.himarket.core.exception.ErrorCode; -import com.alibaba.himarket.dto.params.chat.ChatContext; -import com.alibaba.himarket.dto.params.chat.InvokeModelParam; import com.alibaba.himarket.dto.params.chat.McpToolMeta; import com.alibaba.himarket.dto.params.chat.ToolContext; import com.alibaba.himarket.dto.result.chat.ChatAnswerMessage; -import com.alibaba.himarket.dto.result.chat.LlmChatRequest; +import com.alibaba.himarket.dto.result.chat.LlmChatRequestLegacy; import com.alibaba.himarket.dto.result.chat.LlmInvokeResult; import com.alibaba.himarket.dto.result.consumer.CredentialContext; import com.alibaba.himarket.dto.result.model.ModelConfigResult; import com.alibaba.himarket.dto.result.product.ProductResult; -import com.alibaba.himarket.service.LlmService; import com.alibaba.himarket.support.chat.ChatMessage; import com.alibaba.himarket.support.chat.ChatUsage; import com.alibaba.himarket.support.enums.ChatRole; @@ -73,7 +70,8 @@ @Slf4j @RequiredArgsConstructor -public abstract class AbstractLlmService implements LlmService { +@Deprecated +public abstract class AbstractLlmServiceLegacy implements LlmServiceLegacy { protected final ToolCallingManager toolCallingManager; @@ -90,12 +88,12 @@ public abstract class AbstractLlmService implements LlmService { */ @Override public Flux invokeLLM( - InvokeModelParam param, + InvokeModelParamLegacy param, HttpServletResponse response, Consumer resultHandler) { // ResultHandler is mainly used to record answer and usage try { - LlmChatRequest request = composeRequest(param); + LlmChatRequestLegacy request = composeRequest(param); return call(request, resultHandler); } catch (Exception e) { @@ -116,10 +114,10 @@ public Flux invokeLLM( * @return */ private Flux call( - LlmChatRequest request, Consumer resultHandler) { + LlmChatRequestLegacy request, Consumer resultHandler) { request.tryResolveDns(); - ChatContext chatContext = initChatContext(request); + ChatContextLegacy chatContext = initChatContext(request); chatContext.start(); Flux resp = @@ -173,11 +171,7 @@ private Flux call( .doOnComplete(() -> resultHandler.accept(LlmInvokeResult.of(chatContext))); return applyErrorHandling(resp, chatContext, resultHandler) - .doFinally( - s -> { - chatContext.stop(); - chatContext.close(); - }); + .doFinally(s -> chatContext.stop()); } /** @@ -186,7 +180,7 @@ private Flux call( * @param request * @return */ - private ChatContext initChatContext(LlmChatRequest request) { + private ChatContextLegacy initChatContext(LlmChatRequestLegacy request) { Map toolsMap = new HashMap<>(); List mcpClientWrappers = new ArrayList<>(); @@ -252,7 +246,7 @@ private ChatContext initChatContext(LlmChatRequest request) { List messages = transformMessages(request.getChatMessages()); - return ChatContext.builder() + return ChatContextLegacy.builder() .chatId(request.getChatId()) .messages(messages) .chatClient(chatClient) @@ -268,7 +262,7 @@ private ChatContext initChatContext(LlmChatRequest request) { * @param param * @return */ - private LlmChatRequest composeRequest(InvokeModelParam param) { + private LlmChatRequestLegacy composeRequest(InvokeModelParamLegacy param) { // Not be null ProductResult product = param.getProduct(); ModelConfigResult modelConfig = product.getModelConfig(); @@ -287,7 +281,7 @@ private LlmChatRequest composeRequest(InvokeModelParam param) { ? new WebSearchOptions(WebSearchOptions.SearchContextSize.MEDIUM, null) : null; - return LlmChatRequest.builder() + return LlmChatRequestLegacy.builder() .chatId(param.getChatId()) .userQuestion(param.getUserQuestion()) .uri(uri) @@ -335,7 +329,7 @@ private ModelFeature getOrDefaultModelFeature(ProductResult product) { * @return */ private Flux streamToolCalls( - ChatContext chatContext, + ChatContextLegacy chatContext, ChatResponse chatResponse, Consumer resultHandler) { Usage usage = chatResponse.getMetadata().getUsage(); @@ -475,7 +469,7 @@ private Flux streamToolCalls( * @return */ private Flux streamAnswer( - ChatResponse chatResponse, ChatContext chatContext) { + ChatResponse chatResponse, ChatContextLegacy chatContext) { Usage usage = chatResponse.getMetadata().getUsage(); return Flux.fromIterable(chatResponse.getResults()) .map( @@ -503,7 +497,7 @@ private Flux streamAnswer( * @return */ private Flux continueNextCall( - ChatContext chatContext, Consumer resultHandler) { + ChatContextLegacy chatContext, Consumer resultHandler) { ChatOptions chatOptions = chatContext.getChatOptions(); return Flux.defer( @@ -544,7 +538,7 @@ private Flux continueNextCall( */ private Flux applyErrorHandling( Flux flux, - ChatContext chatContext, + ChatContextLegacy chatContext, Consumer resultHandler) { String chatId = chatContext.getChatId(); @@ -585,7 +579,7 @@ private Flux applyErrorHandling( * @return */ private Flux buildToolCallMessages( - List toolCalls, Usage usage, ChatContext chatContext) { + List toolCalls, Usage usage, ChatContextLegacy chatContext) { return Flux.fromIterable(toolCalls) .map( toolCall -> { @@ -623,7 +617,7 @@ private Flux buildToolCallMessages( * @return */ private Flux executeToolCalls( - Usage usage, ChatContext chatContext, ChatResponse chatResponse) { + Usage usage, ChatContextLegacy chatContext, ChatResponse chatResponse) { return Flux.defer( () -> { Stopwatch stopwatch = Stopwatch.createUnstarted(); @@ -695,7 +689,7 @@ private Flux executeToolCalls( * @return */ private ChatAnswerMessage newChatAnswerMessage( - Usage usage, Object content, MessageType messageType, ChatContext chatContext) { + Usage usage, Object content, MessageType messageType, ChatContextLegacy chatContext) { // Append to answer content if (messageType == ANSWER && content instanceof String strContent) { chatContext.appendAnswer(strContent); @@ -705,8 +699,8 @@ private ChatAnswerMessage newChatAnswerMessage( (usage != null && !(usage instanceof EmptyUsage)) ? ChatUsage.builder() .firstByteTimeout(chatContext.getFirstByteTimeout()) - .promptTokens(usage.getPromptTokens()) - .completionTokens(usage.getCompletionTokens()) + .inputTokens(usage.getPromptTokens()) + .outputTokens(usage.getCompletionTokens()) .totalTokens(usage.getTotalTokens()) .build() : null; @@ -791,5 +785,5 @@ protected abstract URI getUri( * @param request * @return */ - protected abstract ChatClient newChatClient(LlmChatRequest request); + protected abstract ChatClient newChatClient(LlmChatRequestLegacy request); } diff --git a/himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/ChatContext.java b/himarket-server/src/main/java/com/alibaba/himarket/service/legacy/ChatContextLegacy.java similarity index 95% rename from himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/ChatContext.java rename to himarket-server/src/main/java/com/alibaba/himarket/service/legacy/ChatContextLegacy.java index 3539aa0cf..9288903d6 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/ChatContext.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/legacy/ChatContextLegacy.java @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -package com.alibaba.himarket.dto.params.chat; +package com.alibaba.himarket.service.legacy; import cn.hutool.core.collection.CollUtil; -import com.alibaba.himarket.service.impl.McpClientWrapper; +import com.alibaba.himarket.dto.params.chat.ToolContext; import com.alibaba.himarket.support.chat.ChatUsage; import com.google.common.base.Stopwatch; import java.io.IOException; @@ -34,7 +34,8 @@ @Data @Builder @Slf4j -public class ChatContext { +@Deprecated +public class ChatContextLegacy { // region Chat Result - Fields for storing chat response and metrics private String chatId; @@ -87,17 +88,7 @@ public void stop() { chatUsage.setFirstByteTimeout(firstByteTimeout); } } - } - - public void appendAnswer(String content) { - answerContent.append(content); - } - - public int nextRound() { - return ++round; - } - public void close() { if (CollUtil.isNotEmpty(mcpClientWrappers)) { mcpClientWrappers.forEach( mcpClientWrapper -> { @@ -109,4 +100,12 @@ public void close() { }); } } + + public void appendAnswer(String content) { + answerContent.append(content); + } + + public int nextRound() { + return ++round; + } } diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/ChatServiceImpl.java b/himarket-server/src/main/java/com/alibaba/himarket/service/legacy/ChatServiceImplLegacy.java similarity index 93% rename from himarket-server/src/main/java/com/alibaba/himarket/service/impl/ChatServiceImpl.java rename to himarket-server/src/main/java/com/alibaba/himarket/service/legacy/ChatServiceImplLegacy.java index 939cf0c9b..4f38f19c5 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/ChatServiceImpl.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/legacy/ChatServiceImplLegacy.java @@ -17,21 +17,19 @@ * under the License. */ -package com.alibaba.himarket.service.impl; +package com.alibaba.himarket.service.legacy; import cn.hutool.core.codec.Base64; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; -import com.alibaba.himarket.core.event.ChatSessionDeletingEvent; import com.alibaba.himarket.core.exception.BusinessException; import com.alibaba.himarket.core.exception.ErrorCode; import com.alibaba.himarket.core.security.ContextHolder; import com.alibaba.himarket.core.utils.CacheUtil; import com.alibaba.himarket.core.utils.IdGenerator; import com.alibaba.himarket.dto.params.chat.CreateChatParam; -import com.alibaba.himarket.dto.params.chat.InvokeModelParam; import com.alibaba.himarket.dto.result.chat.ChatAnswerMessage; import com.alibaba.himarket.dto.result.chat.LlmInvokeResult; import com.alibaba.himarket.dto.result.consumer.CredentialContext; @@ -61,20 +59,19 @@ import java.util.stream.Collectors; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.context.event.EventListener; import org.springframework.data.domain.Sort; -import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; @Service @Slf4j @AllArgsConstructor -public class ChatServiceImpl implements ChatService { +@Deprecated +public class ChatServiceImplLegacy implements ChatServiceLegacy { private final ChatSessionService sessionService; - private final LlmService llmService; + private final LlmServiceLegacy llmServiceLegacy; private final ChatRepository chatRepository; @@ -106,11 +103,12 @@ public Flux chat(CreateChatParam param, HttpServletResponse r List chatMessages = mergeAndTruncateMessages(currentMessage, historyMessages); - InvokeModelParam invokeModelParam = buildInvokeModelParam(param, chatMessages, chat); + InvokeModelParamLegacy invokeModelParamLegacy = + buildInvokeModelParam(param, chatMessages, chat); // Invoke LLM - return llmService.invokeLLM( - invokeModelParam, response, r -> updateChatResult(chat.getChatId(), r)); + return llmServiceLegacy.invokeLLM( + invokeModelParamLegacy, response, r -> updateChatResult(chat.getChatId(), r)); } private Chat createChat(CreateChatParam param) { @@ -356,7 +354,7 @@ private List mergeAndTruncateMessages( return messages; } - private InvokeModelParam buildInvokeModelParam( + private InvokeModelParamLegacy buildInvokeModelParam( CreateChatParam param, List chatMessages, Chat chat) { // Get product config ProductResult productResult = productService.getProduct(param.getProductId()); @@ -370,7 +368,7 @@ private InvokeModelParam buildInvokeModelParam( CredentialContext credentialContext = consumerService.getDefaultCredential(contextHolder.getUser()); - return InvokeModelParam.builder() + return InvokeModelParamLegacy.builder() .chatId(chat.getChatId()) .userQuestion(param.getQuestion()) .product(productResult) @@ -408,19 +406,4 @@ private List buildMCPConfigs(CreateChatParam param) { .map(product -> product.getMcpConfig().toTransportConfig()) .collect(Collectors.toList()); } - - @EventListener - @Async("taskExecutor") - @Override - public void handleSessionDeletion(ChatSessionDeletingEvent event) { - String sessionId = event.getSessionId(); - try { - chatRepository.deleteAllBySessionId(sessionId); - - log.info("Completed cleanup chat records for session {}", sessionId); - } catch (Exception e) { - log.error( - "Failed to cleanup chat records for session {}: {}", sessionId, e.getMessage()); - } - } } diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/ChatService.java b/himarket-server/src/main/java/com/alibaba/himarket/service/legacy/ChatServiceLegacy.java similarity index 78% rename from himarket-server/src/main/java/com/alibaba/himarket/service/ChatService.java rename to himarket-server/src/main/java/com/alibaba/himarket/service/legacy/ChatServiceLegacy.java index b185c738b..833aae7ba 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/ChatService.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/legacy/ChatServiceLegacy.java @@ -17,15 +17,15 @@ * under the License. */ -package com.alibaba.himarket.service; +package com.alibaba.himarket.service.legacy; -import com.alibaba.himarket.core.event.ChatSessionDeletingEvent; import com.alibaba.himarket.dto.params.chat.CreateChatParam; import com.alibaba.himarket.dto.result.chat.ChatAnswerMessage; import jakarta.servlet.http.HttpServletResponse; import reactor.core.publisher.Flux; -public interface ChatService { +@Deprecated +public interface ChatServiceLegacy { /** * Perform a chat @@ -36,10 +36,10 @@ public interface ChatService { */ Flux chat(CreateChatParam param, HttpServletResponse response); - /** - * Handle session deletion event, such as cleaning up all related chat records - * - * @param event - */ - void handleSessionDeletion(ChatSessionDeletingEvent event); + // /** + // * Handle session deletion event, such as cleaning up all related chat records + // * + // * @param event + // */ + // void handleSessionDeletion(ChatSessionDeletingEvent event); } diff --git a/himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/InvokeModelParam.java b/himarket-server/src/main/java/com/alibaba/himarket/service/legacy/InvokeModelParamLegacy.java similarity index 94% rename from himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/InvokeModelParam.java rename to himarket-server/src/main/java/com/alibaba/himarket/service/legacy/InvokeModelParamLegacy.java index 894ac0605..042ef88d3 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/dto/params/chat/InvokeModelParam.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/legacy/InvokeModelParamLegacy.java @@ -17,7 +17,7 @@ * under the License. */ -package com.alibaba.himarket.dto.params.chat; +package com.alibaba.himarket.service.legacy; import com.alibaba.himarket.dto.result.consumer.CredentialContext; import com.alibaba.himarket.dto.result.product.ProductResult; @@ -30,7 +30,8 @@ @Data @Builder -public class InvokeModelParam { +@Deprecated +public class InvokeModelParamLegacy { /** Unique ID */ private String chatId; diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/LlmService.java b/himarket-server/src/main/java/com/alibaba/himarket/service/legacy/LlmServiceLegacy.java similarity index 89% rename from himarket-server/src/main/java/com/alibaba/himarket/service/LlmService.java rename to himarket-server/src/main/java/com/alibaba/himarket/service/legacy/LlmServiceLegacy.java index d6bbdbe58..d1b826b9d 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/LlmService.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/legacy/LlmServiceLegacy.java @@ -17,9 +17,8 @@ * under the License. */ -package com.alibaba.himarket.service; +package com.alibaba.himarket.service.legacy; -import com.alibaba.himarket.dto.params.chat.InvokeModelParam; import com.alibaba.himarket.dto.result.chat.ChatAnswerMessage; import com.alibaba.himarket.dto.result.chat.LlmInvokeResult; import com.alibaba.himarket.support.enums.AIProtocol; @@ -27,7 +26,8 @@ import java.util.function.Consumer; import reactor.core.publisher.Flux; -public interface LlmService { +@Deprecated +public interface LlmServiceLegacy { /** * Chat with LLM @@ -38,7 +38,7 @@ public interface LlmService { * @return */ Flux invokeLLM( - InvokeModelParam param, + InvokeModelParamLegacy param, HttpServletResponse response, Consumer resultHandler); diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/McpClientFactory.java b/himarket-server/src/main/java/com/alibaba/himarket/service/legacy/McpClientFactory.java similarity index 98% rename from himarket-server/src/main/java/com/alibaba/himarket/service/impl/McpClientFactory.java rename to himarket-server/src/main/java/com/alibaba/himarket/service/legacy/McpClientFactory.java index db32ab6db..bd520971e 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/McpClientFactory.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/legacy/McpClientFactory.java @@ -17,7 +17,7 @@ * under the License. */ -package com.alibaba.himarket.service.impl; +package com.alibaba.himarket.service.legacy; import cn.hutool.core.map.MapUtil; import com.alibaba.himarket.dto.result.consumer.CredentialContext; @@ -38,6 +38,7 @@ import org.springframework.web.util.UriComponentsBuilder; @Slf4j +@Deprecated public class McpClientFactory { public static McpClientWrapper newClient( diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/McpClientWrapper.java b/himarket-server/src/main/java/com/alibaba/himarket/service/legacy/McpClientWrapper.java similarity index 97% rename from himarket-server/src/main/java/com/alibaba/himarket/service/impl/McpClientWrapper.java rename to himarket-server/src/main/java/com/alibaba/himarket/service/legacy/McpClientWrapper.java index ba63ab229..c0e311c7e 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/McpClientWrapper.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/legacy/McpClientWrapper.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package com.alibaba.himarket.service.impl; +package com.alibaba.himarket.service.legacy; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.spec.McpSchema; @@ -29,6 +29,7 @@ @Data @Slf4j +@Deprecated public class McpClientWrapper implements Closeable { private McpSyncClient mcpSyncClient; diff --git a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/OpenAILlmService.java b/himarket-server/src/main/java/com/alibaba/himarket/service/legacy/OpenAILlmServiceLegacy.java similarity index 95% rename from himarket-server/src/main/java/com/alibaba/himarket/service/impl/OpenAILlmService.java rename to himarket-server/src/main/java/com/alibaba/himarket/service/legacy/OpenAILlmServiceLegacy.java index e27c13723..582bc4cf7 100644 --- a/himarket-server/src/main/java/com/alibaba/himarket/service/impl/OpenAILlmService.java +++ b/himarket-server/src/main/java/com/alibaba/himarket/service/legacy/OpenAILlmServiceLegacy.java @@ -17,12 +17,12 @@ * under the License. */ -package com.alibaba.himarket.service.impl; +package com.alibaba.himarket.service.legacy; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.StrUtil; -import com.alibaba.himarket.dto.result.chat.LlmChatRequest; +import com.alibaba.himarket.dto.result.chat.LlmChatRequestLegacy; import com.alibaba.himarket.dto.result.common.DomainResult; import com.alibaba.himarket.dto.result.consumer.CredentialContext; import com.alibaba.himarket.dto.result.httpapi.HttpRouteResult; @@ -46,14 +46,15 @@ @Service @Slf4j -public class OpenAILlmService extends AbstractLlmService { +@Deprecated +public class OpenAILlmServiceLegacy extends AbstractLlmServiceLegacy { - public OpenAILlmService(ToolCallingManager toolCallingManager) { + public OpenAILlmServiceLegacy(ToolCallingManager toolCallingManager) { super(toolCallingManager); } @Override - public ChatClient newChatClient(LlmChatRequest request) { + public ChatClient newChatClient(LlmChatRequestLegacy request) { MultiValueMap headers = new HttpHeaders(); Optional.ofNullable(request.getHeaders()) .ifPresent(headerMap -> headerMap.forEach(headers::add)); diff --git a/himarket-web/himarket-admin/src/components/api-product/ModelFeatureForm.tsx b/himarket-web/himarket-admin/src/components/api-product/ModelFeatureForm.tsx index c55188b5b..7f47e12f5 100644 --- a/himarket-web/himarket-admin/src/components/api-product/ModelFeatureForm.tsx +++ b/himarket-web/himarket-admin/src/components/api-product/ModelFeatureForm.tsx @@ -44,14 +44,14 @@ export default function ModelFeatureForm({ initialExpanded = false }: ModelFeatu diff --git a/himarket-web/himarket-frontend/src/components/ProductHeader.tsx b/himarket-web/himarket-frontend/src/components/ProductHeader.tsx index 8cab12c4a..431bbf616 100644 --- a/himarket-web/himarket-frontend/src/components/ProductHeader.tsx +++ b/himarket-web/himarket-frontend/src/components/ProductHeader.tsx @@ -4,7 +4,7 @@ import { ApiOutlined, CheckCircleFilled, ClockCircleFilled, ExclamationCircleFil import { useParams } from "react-router-dom"; import { getConsumers, subscribeProduct, unsubscribeProduct, getProductSubscriptions } from "../lib/api"; import type { Consumer } from "../types/consumer"; -import type { IMCPConfig, IProductIcon } from "../lib/apis/typing"; +import type { IMCPConfig, IProductIcon, IAgentConfig } from "../lib/apis/typing"; import APIs, { getProductSubscriptionStatus, type ISubscription } from "../lib/apis"; const { Title, Paragraph } = Typography; @@ -16,7 +16,7 @@ interface ProductHeaderProps { icon?: IProductIcon; defaultIcon?: string; mcpConfig?: IMCPConfig; - agentConfig?: any; // 添加 agentConfig 支持,用于判断 Agent 来源 + agentConfig?: IAgentConfig; updatedAt?: string; productType?: 'REST_API' | 'MCP_SERVER' | 'AGENT_API' | 'MODEL_API'; } @@ -108,7 +108,7 @@ export const ProductHeader: React.FC = ({ const productId = apiProductId || mcpProductId || agentProductId || modelProductId || ''; // 查询订阅状态 - const fetchSubscriptionStatus = async () => { + const fetchSubscriptionStatus = React.useCallback(async () => { if (!productId || !shouldShowSubscribeButton) return; setSubscriptionLoading(true); @@ -120,7 +120,7 @@ export const ProductHeader: React.FC = ({ } finally { setSubscriptionLoading(false); } - }; + }, [productId, shouldShowSubscribeButton]); // 获取订阅详情(用于管理弹窗) const fetchSubscriptionDetails = async (page: number = 1, search: string = ''): Promise => { @@ -149,7 +149,7 @@ export const ProductHeader: React.FC = ({ useEffect(() => { fetchSubscriptionStatus(); - }, [productId, shouldShowSubscribeButton]); + }, [fetchSubscriptionStatus]); // 获取消费者列表 const fetchConsumers = async () => { diff --git a/himarket-web/himarket-frontend/src/components/SwaggerUIWrapper.tsx b/himarket-web/himarket-frontend/src/components/SwaggerUIWrapper.tsx index 8fc599f34..56f7aa7f0 100644 --- a/himarket-web/himarket-frontend/src/components/SwaggerUIWrapper.tsx +++ b/himarket-web/himarket-frontend/src/components/SwaggerUIWrapper.tsx @@ -11,28 +11,29 @@ interface SwaggerUIWrapperProps { export const SwaggerUIWrapper: React.FC = ({ apiSpec }) => { // 直接解析原始规范,不进行重新构建 - let swaggerSpec: any; + let swaggerSpec: Record; try { // 尝试解析YAML格式 try { - swaggerSpec = yaml.load(apiSpec); + swaggerSpec = yaml.load(apiSpec) as Record; } catch { // 如果YAML解析失败,尝试JSON格式 - swaggerSpec = JSON.parse(apiSpec); + swaggerSpec = JSON.parse(apiSpec) as Record; } - if (!swaggerSpec || !swaggerSpec.paths) { + if (!swaggerSpec || typeof swaggerSpec !== 'object' || !('paths' in swaggerSpec)) { throw new Error('Invalid OpenAPI specification'); } // 为没有tags的操作添加默认标签,避免显示"default" - Object.keys(swaggerSpec.paths).forEach(path => { - const pathItem = swaggerSpec.paths[path]; + const paths = swaggerSpec.paths as Record>; + Object.keys(paths).forEach(path => { + const pathItem = paths[path]; Object.keys(pathItem).forEach(method => { const operation = pathItem[method]; - if (operation && typeof operation === 'object' && !operation.tags) { - operation.tags = ['接口列表']; + if (operation && typeof operation === 'object' && !('tags' in operation)) { + (operation as Record).tags = ['接口列表']; } }); }); diff --git a/himarket-web/himarket-frontend/src/components/TextType.tsx b/himarket-web/himarket-frontend/src/components/TextType.tsx index 596301eee..79006324f 100644 --- a/himarket-web/himarket-frontend/src/components/TextType.tsx +++ b/himarket-web/himarket-frontend/src/components/TextType.tsx @@ -65,7 +65,10 @@ const TextType = ({ }; useEffect(() => { - if (!startOnVisible || !containerRef.current) return; + if (!startOnVisible) return; + + const currentContainer = containerRef.current; + if (!currentContainer) return; const observer = new IntersectionObserver( entries => { @@ -78,7 +81,7 @@ const TextType = ({ { threshold: 0.1 } ); - observer.observe(containerRef.current); + observer.observe(currentContainer); return () => observer.disconnect(); }, [startOnVisible]); diff --git a/himarket-web/himarket-frontend/src/components/chat/Area.tsx b/himarket-web/himarket-frontend/src/components/chat/Area.tsx index 000abc52c..ad5068260 100644 --- a/himarket-web/himarket-frontend/src/components/chat/Area.tsx +++ b/himarket-web/himarket-frontend/src/components/chat/Area.tsx @@ -82,7 +82,7 @@ export function ChatArea(props: ChatAreaProps) { categoryIds: ['all', 'added'].includes(id) ? [] : [id], }); } - }, [addedMcps]); + }, [addedMcps, setMcpList, getMcpList]); const handleMcpSearch = useCallback((id: string, name: string) => { if (id === "added") { @@ -94,7 +94,7 @@ export function ChatArea(props: ChatAreaProps) { name }); } - }, [addedMcps]); + }, [addedMcps, getMcpList]); const toggleMcpModal = useCallback(() => { setShowMcpModal(v => !v); diff --git a/himarket-web/himarket-frontend/src/components/chat/McpToolCallPanel.tsx b/himarket-web/himarket-frontend/src/components/chat/McpToolCallPanel.tsx index f12a7b770..9d4d89a90 100644 --- a/himarket-web/himarket-frontend/src/components/chat/McpToolCallPanel.tsx +++ b/himarket-web/himarket-frontend/src/components/chat/McpToolCallPanel.tsx @@ -25,23 +25,24 @@ export function McpToolCallPanel({ toolCalls = [], toolResponses = [] }: McpTool
{toolItems.map(({ toolCall, toolResponse }, index) => { const panelKey = `mcp-tool-${index}`; - const mcpServerName = toolCall.toolMeta.mcpNameCn || toolCall.toolMeta.mcpName; - const toolName = toolCall.toolMeta.toolNameCn || toolCall.toolMeta.toolName; + // @chat-legacy: Legacy fallback logic removed + const mcpServerName = toolCall.mcpServerName; // || toolCall.toolMeta?.mcpNameCn || toolCall.toolMeta?.mcpName; + const toolName = toolCall.name; // || toolCall.toolMeta?.toolNameCn || toolCall.toolMeta?.toolName; - // 解析 JSON 字符串 // eslint-disable-next-line @typescript-eslint/no-explicit-any let parsedInput: any = null; // eslint-disable-next-line @typescript-eslint/no-explicit-any let parsedResponse: any = null; try { - parsedInput = JSON.parse(toolCall.input || toolCall.arguments || "{}"); + parsedInput = JSON.parse(toolCall.arguments || "{}"); // @chat-legacy: || toolCall.input } catch { - parsedInput = toolCall.input || toolCall.arguments; + parsedInput = toolCall.arguments; // @chat-legacy: || toolCall.input; } try { - parsedResponse = JSON.parse(toolResponse?.responseData || toolResponse?.output || "{}"); + const resultString = typeof toolResponse?.result === 'string' ? toolResponse.result : JSON.stringify(toolResponse?.result || {}); + parsedResponse = JSON.parse(resultString || "{}"); // @chat-legacy: || toolResponse?.responseData || toolResponse?.output } catch { - parsedResponse = toolResponse?.responseData || toolResponse?.output; + parsedResponse = toolResponse?.result; // @chat-legacy: || toolResponse?.responseData || toolResponse?.output; } return ( diff --git a/himarket-web/himarket-frontend/src/components/chat/Messages.tsx b/himarket-web/himarket-frontend/src/components/chat/Messages.tsx index cad96540b..67ecbdd5d 100644 --- a/himarket-web/himarket-frontend/src/components/chat/Messages.tsx +++ b/himarket-web/himarket-frontend/src/components/chat/Messages.tsx @@ -68,7 +68,7 @@ function Message({ }: { conversation: IModelConversation["conversations"][0]; question: IModelConversation["conversations"][0]['questions'][0], - activeAnswer: IModelConversation["conversations"][0]['questions'][0]['answers'][0], + activeAnswer?: IModelConversation["conversations"][0]['questions'][0]['answers'][0], modelIcon?: string; modelName?: string; isNewChat?: boolean; isLast: boolean; onChangeVersion?: (conversationId: string, questionId: string, direction: 'prev' | 'next') => void; @@ -77,7 +77,10 @@ function Message({ const contentRef = useRef(null); - const [expandedContent, setExpandedContent] = useState(true); + const [expandedContent, setExpandedContent] = useState(() => { + // Initial state will be updated after first render + return true; + }); const [copiedId, setCopiedId] = useState(null); const handleCopy = async (content: string, messageId: string) => { @@ -95,12 +98,14 @@ function Message({ }; useEffect(() => { + // Check content height after render and update expanded state if needed if (contentRef.current) { - if (contentRef.current.getBoundingClientRect().height < 160) { + const height = contentRef.current.getBoundingClientRect().height; + if (height < 160) { setExpandedContent(false); } } - }, []) + }, [activeAnswer?.content]) return (
@@ -161,7 +166,7 @@ function Message({
) : (
- +
)}
@@ -180,7 +185,7 @@ function Message({ {/* 右侧:功能按钮 */}