Bläddra i källkod

Merge branch 'dev-yhq' into develop

tsurumure 1 månad sedan
förälder
incheckning
9de363eb90
56 ändrade filer med 2426 tillägg och 382 borttagningar
  1. 3 0
      README.md
  2. 1 1
      db/cms_site_info.sql
  3. 1 1
      db/crt_drama_project.sql
  4. 1 1
      db/crt_drama_project_settings.sql
  5. 1 1
      db/crt_drama_project_storyboard.sql
  6. 1 1
      db/crt_drama_task.sql
  7. 23 0
      db/crt_generate_image.sql
  8. 1 1
      db/crt_lora_figure.sql
  9. 1 1
      db/crt_lora_style.sql
  10. 1 1
      db/crt_lora_style_category.sql
  11. 1 1
      db/crt_lora_style_collect.sql
  12. 1 1
      db/crt_model.sql
  13. 1 1
      db/sys_log.sql
  14. 1 1
      db/sys_mobile_area_code.sql
  15. 1 1
      db/sys_resource_points.sql
  16. 1 1
      db/sys_sms.sql
  17. 1 1
      db/sys_sms_history.sql
  18. 1 1
      db/sys_user.sql
  19. 1 1
      db/sys_user_info.sql
  20. 1 1
      db/sys_user_integral_log.sql
  21. 1 1
      db/sys_user_points_history.sql
  22. 1 1
      db/sys_user_role.sql
  23. 5 1
      db/sys_user_role_permission.sql
  24. 3 2
      db/sys_user_role_permission_relation.sql
  25. 1 1
      db/sys_user_role_relation.sql
  26. 12 5
      pom.xml
  27. 5 2
      src/main/java/com/backendsys/config/ThreadPool/ThreadPoolConfig.java
  28. 107 107
      src/main/java/com/backendsys/config/WebSocket/WebSocketConfig.java
  29. 718 0
      src/main/java/com/backendsys/modules/crt/comfyui-api.md
  30. 42 0
      src/main/java/com/backendsys/modules/crt/controller/CrtGenerateController.java
  31. 9 0
      src/main/java/com/backendsys/modules/crt/dao/CrtGenerateImageDao.java
  32. 3 1
      src/main/java/com/backendsys/modules/crt/entity/CrtDramaProjectStoryboard.java
  33. 1 1
      src/main/java/com/backendsys/modules/crt/entity/CrtDramaTask.java
  34. 27 0
      src/main/java/com/backendsys/modules/crt/entity/CrtGenerateImage.java
  35. 15 0
      src/main/java/com/backendsys/modules/crt/service/CrtGenerateService.java
  36. 156 0
      src/main/java/com/backendsys/modules/crt/service/impl/CrtGenerateServiceImpl.java
  37. 0 14
      src/main/java/com/backendsys/modules/sdk/baidu/bce/utils/BaiduBceUtil.java
  38. 50 0
      src/main/java/com/backendsys/modules/sdk/comfyui/controller/ComfyUIDemoController.java
  39. 12 0
      src/main/java/com/backendsys/modules/sdk/comfyui/entity/CFPromptRequest.java
  40. 13 0
      src/main/java/com/backendsys/modules/sdk/comfyui/entity/CFPromptResponse.java
  41. 10 0
      src/main/java/com/backendsys/modules/sdk/comfyui/entity/CFQueue.java
  42. 24 0
      src/main/java/com/backendsys/modules/sdk/comfyui/enums/TypeEnums.java
  43. 16 0
      src/main/java/com/backendsys/modules/sdk/comfyui/service/ComfyUIService.java
  44. 16 0
      src/main/java/com/backendsys/modules/sdk/comfyui/service/ComfyUISocketService.java
  45. 93 0
      src/main/java/com/backendsys/modules/sdk/comfyui/service/impl/ComfyUIServiceImpl.java
  46. 213 0
      src/main/java/com/backendsys/modules/sdk/comfyui/service/impl/ComfyUISocketServiceImpl.java
  47. 8 0
      src/main/java/com/backendsys/modules/sdk/comfyui/utils/ComfyUtil.java
  48. 3 1
      src/main/java/com/backendsys/modules/sdk/deepseek/service/impl/DeepSeekClientImpl.java
  49. 127 220
      src/main/java/com/backendsys/modules/sdk/deepseek/utils/OllamaUtil.java
  50. 378 0
      src/main/java/com/backendsys/modules/sdk/deepseek/utils/OllamaUtil_bak.java
  51. 0 4
      src/main/java/com/backendsys/modules/sdk/tencentcloud/huanyuan/service/impl/HunYuanClientImpl.java
  52. 297 0
      src/main/java/com/backendsys/modules/sdk/tencentcloud/huanyuan/service/impl/HunYuanClientImpl222.java
  53. 1 0
      src/main/java/com/backendsys/modules/sse/entity/SseResponseEnum.java
  54. 5 1
      src/main/resources/application-dev.yml
  55. 5 1
      src/main/resources/application-local.yml
  56. 5 1
      src/main/resources/application-prod.yml

+ 3 - 0
README.md

@@ -127,6 +127,9 @@ Long user_count = Convert.toLong(sysUserRole.get("user_count"));
 - cmd + Shift + F 将 com.xxx 名称进行全局替换;
 - 将 src/main/java/com/xxx 名称进行修改
 
+#### 查看依赖树
+> 可查看依赖中是否含有冲突项(例如 okhttp)
+$ mvn dependency:tree
 
 ### Future (待加功能)
 1.sys_dictionary.sql 字典表重构

+ 1 - 1
db/cms_site_info.sql

@@ -7,7 +7,7 @@ Date: 2023/05/23 17:09:22
 DROP TABLE IF EXISTS `cms_site_info`;
 CREATE TABLE `cms_site_info` (
     PRIMARY KEY (`id`),
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `language` VARCHAR(10) NOT NULL COMMENT '语种',
     `name` VARCHAR(20) NOT NULL COMMENT '网站名称',
     `meta_keyword` VARCHAR(255) COMMENT 'SEO-Keyword',

+ 1 - 1
db/crt_drama_project.sql

@@ -7,7 +7,7 @@ Date: 2025/06/03 10:09:22
 DROP TABLE IF EXISTS `crt_drama_project`;
 CREATE TABLE `crt_drama_project` (
     PRIMARY KEY (`id`),
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `user_id` BIGINT NOT NULL COMMENT '用户ID (拥有者)',
     `project_name` VARCHAR(50) NOT NULL COMMENT '项目名称',
     `drama_lora_style_id` BIGINT COMMENT '风格LoRA ID',

+ 1 - 1
db/crt_drama_project_settings.sql

@@ -7,7 +7,7 @@ Date: 2025/06/03 10:09:22
 DROP TABLE IF EXISTS `crt_drama_project_settings`;
 CREATE TABLE `crt_drama_project_settings` (
     PRIMARY KEY (`id`),
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `drama_project_id` BIGINT NOT NULL COMMENT '项目ID',
     `drama_project_setting_type` INT NOT NULL COMMENT '项目配置类型 (1: 生图配置, 2: 生视频配置)',
     `aspect_ratio` VARCHAR(10) COMMENT '画面比例 (枚举)(16:9 - 1280*720, 9:16 - 720*1280, 1:1 - 1024*1024)',

+ 1 - 1
db/crt_drama_project_storyboard.sql

@@ -7,7 +7,7 @@ Date: 2025/06/03 10:09:22
 DROP TABLE IF EXISTS `crt_drama_project_storyboard`;
 CREATE TABLE `crt_drama_project_storyboard` (
     PRIMARY KEY (`id`),
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `user_id` BIGINT NOT NULL COMMENT '用户ID',
 
     `drama_project_id` BIGINT NOT NULL COMMENT '项目ID',

+ 1 - 1
db/crt_drama_task.sql

@@ -7,7 +7,7 @@ Date: 2025/06/03 10:09:22
 DROP TABLE IF EXISTS `crt_drama_task`;
 CREATE TABLE `crt_drama_task` (
     PRIMARY KEY (`id`),
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `user_id` BIGINT NOT NULL COMMENT '用户ID',
     `drama_project_id` BIGINT NOT NULL COMMENT '项目ID',
     `task_id` VARCHAR(255) NOT NULL COMMENT '任务ID',

+ 23 - 0
db/crt_generate_image.sql

@@ -0,0 +1,23 @@
+/**
+Source Server Version: 8.0.31
+Source Database: backendsys
+Date: 2025/06/03 10:09:22
+*/
+
+DROP TABLE IF EXISTS `crt_generate_image`;
+CREATE TABLE `crt_generate_image` (
+    PRIMARY KEY (`id`),
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
+    `user_id` BIGINT COMMENT '用户ID',
+    `name` VARCHAR(255) NOT NULL COMMENT '图片名称',
+    `url_origin` VARCHAR(2000) NOT NULL COMMENT '原图',
+    `url` VARCHAR(2000) COMMENT '转存图',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    INDEX `idx_user_id` (`user_id`)
+) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='短剧创作-生成图片记录表';
+
+# INSERT INTO crt_generate_image(id, name, lora_figure_name, lora_figure_path) VALUES
+#     (1, '柳王妃', 'liuwangfei-0012', '/etc/ComfyUI/custom_nodes/xxx/liuwangfei-0012.safetensors'),
+#     (2, '陆司明', 'lusiming-0001', '/etc/ComfyUI/custom_nodes/xxx/lusiming-0001.safetensors')
+# ;

+ 1 - 1
db/crt_lora_figure.sql

@@ -7,7 +7,7 @@ Date: 2025/06/03 10:09:22
 DROP TABLE IF EXISTS `crt_lora_figure`;
 CREATE TABLE `crt_lora_figure` (
     PRIMARY KEY (`id`),
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `name` VARCHAR(255) NOT NULL COMMENT '人物名称',
     `lora_figure_name` VARCHAR(255) NOT NULL COMMENT '人物LoRA名称',
     `lora_figure_path` VARCHAR(500) NOT NULL COMMENT '人物LoRA路径'

+ 1 - 1
db/crt_lora_style.sql

@@ -7,7 +7,7 @@ Date: 2025/06/03 10:09:22
 DROP TABLE IF EXISTS `crt_lora_style`;
 CREATE TABLE `crt_lora_style` (
     PRIMARY KEY (`id`),
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `category_id` BIGINT NOT NULL COMMENT '风格分类ID',
     `name` VARCHAR(255) NOT NULL COMMENT '风格名称',
     `thumb` VARCHAR(1000) COMMENT '风格缩略图',

+ 1 - 1
db/crt_lora_style_category.sql

@@ -7,7 +7,7 @@ Date: 2025/06/03 10:09:22
 DROP TABLE IF EXISTS `crt_lora_style_category`;
 CREATE TABLE `crt_lora_style_category` (
     PRIMARY KEY (`id`),
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `category_name` VARCHAR(255) NOT NULL COMMENT '风格分类名称'
 ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='短剧创作-风格分类表';
 

+ 1 - 1
db/crt_lora_style_collect.sql

@@ -7,7 +7,7 @@ Date: 2025/06/03 10:09:22
 DROP TABLE IF EXISTS `crt_lora_style_collect`;
 CREATE TABLE `crt_lora_style_collect` (
     PRIMARY KEY (`id`),
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `user_id` BIGINT NOT NULL COMMENT '用户ID',
     `lora_style_id` BIGINT NOT NULL COMMENT '风格ID',
     `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

+ 1 - 1
db/crt_model.sql

@@ -7,7 +7,7 @@ Date: 2025/06/03 10:09:22
 DROP TABLE IF EXISTS `crt_model`;
 CREATE TABLE `crt_model` (
     PRIMARY KEY (`id`),
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `name` VARCHAR(50) NOT NULL COMMENT '名称',
     `model_name` VARCHAR(255) NOT NULL COMMENT '模型名称',
     `model_path` VARCHAR(255) NOT NULL COMMENT '模型路径'

+ 1 - 1
db/sys_log.sql

@@ -7,7 +7,7 @@ Date: 2024/12/28 16:37:00
 DROP TABLE IF EXISTS `sys_log`;
 CREATE TABLE `sys_log` (
     PRIMARY KEY (`id`),
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `username` VARCHAR(255) COMMENT '用户名',
     `action` VARCHAR(255) COMMENT '用户操作',
     `classname` VARCHAR(255) COMMENT '请求类名',

+ 1 - 1
db/sys_mobile_area_code.sql

@@ -7,7 +7,7 @@ Date: 2024/04/24 18:20:21
 DROP TABLE IF EXISTS `sys_mobile_area_code`;
 CREATE TABLE `sys_mobile_area_code` (
     PRIMARY KEY (`id`),
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `area_name` VARCHAR(50) NOT NULL COMMENT '地区名称',
     `area_name_en` VARCHAR(50) NOT NULL COMMENT '地区名称 (英文)',
     `area_name_abbr` VARCHAR(20) COMMENT '地区名称缩写',

+ 1 - 1
db/sys_resource_points.sql

@@ -7,7 +7,7 @@ Date: 2024/04/09 11:21:58
 DROP TABLE IF EXISTS `sys_resource_points`;
 CREATE TABLE `sys_resource_points` (
     PRIMARY KEY (`id`),
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `sign` INT COMMENT '参数值标志',
     `resource_type` VARCHAR(255) NOT NULL COMMENT '资源类型',
     `resource_tag` VARCHAR(255) NOT NULL COMMENT '资源标识',

+ 1 - 1
db/sys_sms.sql

@@ -7,7 +7,7 @@ Date: 2023/05/23 17:09:22
 DROP TABLE IF EXISTS `sys_sms`;
 CREATE TABLE `sys_sms` (
     PRIMARY KEY (`id`),
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `origin` VARCHAR(20) COMMENT '来源',
     `phone_area_code` VARCHAR(20) COMMENT '区号/国家码',
     `phone` VARCHAR(20) NOT NULL COMMENT '手机号码',

+ 1 - 1
db/sys_sms_history.sql

@@ -9,7 +9,7 @@
 # DROP TABLE IF EXISTS `sys_mobile_sms_history`;
 # CREATE TABLE `sys_mobile_sms_history` (
 #     PRIMARY KEY (`id`),
-#     `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+#     `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
 #     `origin` VARCHAR(20) COMMENT '来源',
 #     `phone_area_code` VARCHAR(20) COMMENT '区号/国家码',
 #     `phone` VARCHAR(20) NOT NULL COMMENT '手机号码',

+ 1 - 1
db/sys_user.sql

@@ -7,7 +7,7 @@ Date: 2023/05/23 17:09:22
 DROP TABLE IF EXISTS `sys_user`;
 CREATE TABLE `sys_user` (
     PRIMARY KEY (`id`),
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `username` VARCHAR(20) COMMENT '用户名',
     `phone` VARCHAR(20) COMMENT '手机号码',
     `phone_area_code` INT COMMENT '手机区号/国家码',

+ 1 - 1
db/sys_user_info.sql

@@ -7,7 +7,7 @@ Date: 2023/05/23 17:09:22
 DROP TABLE IF EXISTS `sys_user_info`;
 CREATE TABLE `sys_user_info` (
     PRIMARY KEY (`id`),
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `user_id` BIGINT NOT NULL COMMENT '系统用户ID',
     `nickname` VARCHAR(20) COMMENT '昵称',
     `email` VARCHAR(50) COMMENT '邮箱',

+ 1 - 1
db/sys_user_integral_log.sql

@@ -7,7 +7,7 @@ Date: 2023/05/23 17:09:22
 DROP TABLE IF EXISTS `sys_user_integral_log`;
 CREATE TABLE `sys_user_integral_log` (
     PRIMARY KEY (`id`),
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `user_id` BIGINT NOT NULL COMMENT '用户ID',
     `content` VARCHAR(500) COMMENT '内容',
     `integral` INT NOT NULL COMMENT '操作积分',

+ 1 - 1
db/sys_user_points_history.sql

@@ -7,7 +7,7 @@ Date: 2024/04/09 11:21:58
 DROP TABLE IF EXISTS `sys_user_points_history`;
 CREATE TABLE `sys_user_points_history` (
     PRIMARY KEY (`id`),
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `target_user_id` BIGINT NOT NULL COMMENT '积分生效用户ID',
     `point_adjustment` FLOAT NOT NULL COMMENT '积分变动 (增加|减少) (正|负)',
     `point_balance` FLOAT NOT NULL COMMENT '积分余额',

+ 1 - 1
db/sys_user_role.sql

@@ -7,7 +7,7 @@ Date: 2023/05/23 17:09:22
 DROP TABLE IF EXISTS `sys_user_role`;
 CREATE TABLE `sys_user_role` (
     PRIMARY KEY (`id`),
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `role_name` VARCHAR(20) NOT NULL COMMENT '角色名称',
     `role_description` VARCHAR(200) COMMENT '角色描述',
     `sort` INT DEFAULT '1' COMMENT '排序',

+ 5 - 1
db/sys_user_role_permission.sql

@@ -58,8 +58,12 @@ INSERT INTO sys_user_role_permission(id, parent_id, permission_name, sort) VALUE
                 ('36.2.5.1', '36.2.5', 'AI短剧创作-清空分镜 (自己)', null),
             ('36.2.6', '36.2', 'AI短剧创作-删除分集 (全权限)', null),
                 ('36.2.6.1', '36.2.6', 'AI短剧创作-删除分集 (自己)', null),
+        ('36.3', '36', 'AI短剧创作-生成图片', null),
+        ('36.4', '36', 'AI短剧创作-生成视频', null),
 
-    ('3', -1, '系统用户管理', 900),
+
+
+     ('3', -1, '系统用户管理', 900),
         ('3.1', '3', '系统用户列表 (在线的)', null),
         ('3.2', '3', '系统用户列表', null),
             ('3.2.1', '3.2', '查询用户信息', null),

+ 3 - 2
db/sys_user_role_permission_relation.sql

@@ -7,7 +7,7 @@ Date: 2023/05/23 17:09:22
 DROP TABLE IF EXISTS `sys_user_role_permission_relation`;
 CREATE TABLE `sys_user_role_permission_relation` (
     PRIMARY KEY (`id`),
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `role_id` BIGINT NOT NULL COMMENT '角色ID',
     `permission_id` VARCHAR(10) NOT NULL COMMENT '权限ID',
     INDEX `idx_role_id` (`role_id`)
@@ -116,7 +116,8 @@ INSERT INTO sys_user_role_permission_relation(role_id, permission_id) VALUES
             (1, '36.2.4'), (1, '36.2.4.1'),
             (1, '36.2.5'), (1, '36.2.5.1'),
             (1, '36.2.6'), (1, '36.2.6.1'),
-
+        (1, '36.3'),
+        (1, '36.4'),
 
     (1, '100'),
         (1, '101'),

+ 1 - 1
db/sys_user_role_relation.sql

@@ -7,7 +7,7 @@ Date: 2023/05/23 17:09:22
 DROP TABLE IF EXISTS `sys_user_role_relation`;
 CREATE TABLE `sys_user_role_relation` (
     PRIMARY KEY (`id`),
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `id` BIGINT AUTO_INCREMENT COMMENT 'ID',
     `user_id` BIGINT NOT NULL COMMENT '用户ID',
     `role_id` BIGINT NOT NULL COMMENT '角色ID',
     INDEX `idx_user_id` (`user_id`),

+ 12 - 5
pom.xml

@@ -15,10 +15,17 @@
     <properties>
         <java.version>17</java.version>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <okhttp.version>4.10.0</okhttp.version>
     </properties>
 
     <dependencies>
 
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>${okhttp.version}</version>
+        </dependency>
+
         <!-- Spring Web -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
@@ -217,11 +224,11 @@
         </dependency>
 
         <!-- Retrofit 库中用于添加 HTTP 请求头的注解 -->
-        <dependency>
-            <groupId>com.squareup.retrofit2</groupId>
-            <artifactId>retrofit</artifactId>
-            <version>2.9.0</version>
-        </dependency>
+<!--        <dependency>-->
+<!--            <groupId>com.squareup.retrofit2</groupId>-->
+<!--            <artifactId>retrofit</artifactId>-->
+<!--            <version>2.9.0</version>-->
+<!--        </dependency>-->
 
         <!-- http://doc.wupaas.com/docs/easypoi/easypoi-1c0u97casmdlh -->
         <!-- https://central.sonatype.com/artifact/cn.afterturn/easypoi-base -->

+ 5 - 2
src/main/java/com/backendsys/config/ThreadPool/ThreadPoolConfig.java

@@ -6,6 +6,7 @@ import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 
 import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
 
 /**
  * 线程池
@@ -20,13 +21,15 @@ public class ThreadPoolConfig {
         // 设置核心线程数
         executor.setCorePoolSize(5);
         // 设置最大线程数
-        executor.setMaxPoolSize(20);
+        executor.setMaxPoolSize(10);
         // 配置队列大小
-        executor.setQueueCapacity(Integer.MAX_VALUE);
+        executor.setQueueCapacity(1000);
         // 设置线程活跃时间 (秒)
         executor.setKeepAliveSeconds(60);
         // 等待所有任务结束后再关闭线程池
         executor.setWaitForTasksToCompleteOnShutdown(true);
+        // 设置拒绝策略
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
         // 执行初始化
         executor.initialize();
         return executor;

+ 107 - 107
src/main/java/com/backendsys/config/WebSocket/WebSocketConfig.java

@@ -1,107 +1,107 @@
-package com.backendsys.config.WebSocket;
-
-import cn.hutool.core.util.StrUtil;
-import com.backendsys.modules.common.config.security.utils.JwtUtil;
-
-import io.jsonwebtoken.Claims;
-import lombok.RequiredArgsConstructor;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.http.HttpHeaders;
-import org.springframework.messaging.Message;
-import org.springframework.messaging.MessageChannel;
-import org.springframework.messaging.simp.config.ChannelRegistration;
-import org.springframework.messaging.simp.config.MessageBrokerRegistry;
-import org.springframework.messaging.simp.stomp.StompCommand;
-import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
-import org.springframework.messaging.support.ChannelInterceptor;
-import org.springframework.messaging.support.MessageHeaderAccessor;
-import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
-import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
-import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
-
-@Configuration
-@EnableWebSocketMessageBroker
-@RequiredArgsConstructor
-public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
-
-
-    @Autowired
-    private JwtUtil jwtUtil;
-
-    /**
-     * 注册一个端点,客户端通过这个端点进行连接
-     */
-    @Override
-    public void registerStompEndpoints(StompEndpointRegistry registry) {
-        registry
-                .addEndpoint("/ws")   // 注册了一个 /ws 的端点
-                .setAllowedOriginPatterns("*") // 允许跨域的 WebSocket 连接
-                .withSockJS();  // 启用 SockJS (浏览器不支持WebSocket,SockJS 将会提供兼容性支持)
-    }
-
-    /**
-     * 配置消息代理
-     */
-    @Override
-    public void configureMessageBroker(MessageBrokerRegistry registry) {
-        // 客户端发送消息的请求前缀
-        registry.setApplicationDestinationPrefixes("/app");
-        // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送
-        registry.enableSimpleBroker("/topic", "/queue");
-        // 服务端通知客户端的前缀,可以不设置,默认为user
-        registry.setUserDestinationPrefix("/user");
-    }
-
-    /**
-     * 配置客户端入站通道拦截器
-     * <p>
-     * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户
-     *
-     * @param registration 通道注册器
-     */
-    @Override
-    public void configureClientInboundChannel(ChannelRegistration registration) {
-        registration.interceptors(new ChannelInterceptor() {
-            @Override
-            public Message<?> preSend(Message<?> message, MessageChannel channel) {
-
-                StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
-
-                // (改) 如果是连接请求(CONNECT 命令),从请求头中取出 token 并设置到认证信息中
-                if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
-
-                   // 从连接头中提取授权令牌
-                   String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION);
-
-                   // 验证令牌格式并提取用户信息
-                   if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) {
-                       try {
-                           // 移除 "Bearer " 前缀,从令牌中提取用户信息(username), 并设置到认证信息中
-                           String tokenWithoutPrefix = bearerToken.substring(7);
-
-                           Claims tokenInfo = jwtUtil.extractAllClaims(tokenWithoutPrefix);
-                           String username = (String) tokenInfo.get("username");
-
-                           if (StrUtil.isNotBlank(username)) {
-                               accessor.setUser(() -> username);
-                               return message;
-                           }
-                           
-                       } catch (Exception e) {
-                           throw new RuntimeException("Failed to process authentication token.");
-                       }
-                   }
-
-
-                }
-                // 不是连接请求,直接放行
-
-
-
-                return ChannelInterceptor.super.preSend(message, channel);
-            }
-        });
-    }
-
-}
+//package com.backendsys.config.WebSocket;
+//
+//import cn.hutool.core.util.StrUtil;
+//import com.backendsys.modules.common.config.security.utils.JwtUtil;
+//
+//import io.jsonwebtoken.Claims;
+//import lombok.RequiredArgsConstructor;
+//import org.springframework.beans.factory.annotation.Autowired;
+//import org.springframework.context.annotation.Configuration;
+//import org.springframework.http.HttpHeaders;
+//import org.springframework.messaging.Message;
+//import org.springframework.messaging.MessageChannel;
+//import org.springframework.messaging.simp.config.ChannelRegistration;
+//import org.springframework.messaging.simp.config.MessageBrokerRegistry;
+//import org.springframework.messaging.simp.stomp.StompCommand;
+//import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
+//import org.springframework.messaging.support.ChannelInterceptor;
+//import org.springframework.messaging.support.MessageHeaderAccessor;
+//import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
+//import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
+//import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
+//
+//@Configuration
+//@EnableWebSocketMessageBroker
+//@RequiredArgsConstructor
+//public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
+//
+//
+//    @Autowired
+//    private JwtUtil jwtUtil;
+//
+//    /**
+//     * 注册一个端点,客户端通过这个端点进行连接
+//     */
+//    @Override
+//    public void registerStompEndpoints(StompEndpointRegistry registry) {
+//        registry
+//                .addEndpoint("/ws")   // 注册了一个 /ws 的端点
+//                .setAllowedOriginPatterns("*") // 允许跨域的 WebSocket 连接
+//                .withSockJS();  // 启用 SockJS (浏览器不支持WebSocket,SockJS 将会提供兼容性支持)
+//    }
+//
+//    /**
+//     * 配置消息代理
+//     */
+//    @Override
+//    public void configureMessageBroker(MessageBrokerRegistry registry) {
+//        // 客户端发送消息的请求前缀
+//        registry.setApplicationDestinationPrefixes("/app");
+//        // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送
+//        registry.enableSimpleBroker("/topic", "/queue");
+//        // 服务端通知客户端的前缀,可以不设置,默认为user
+//        registry.setUserDestinationPrefix("/user");
+//    }
+//
+//    /**
+//     * 配置客户端入站通道拦截器
+//     * <p>
+//     * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户
+//     *
+//     * @param registration 通道注册器
+//     */
+//    @Override
+//    public void configureClientInboundChannel(ChannelRegistration registration) {
+//        registration.interceptors(new ChannelInterceptor() {
+//            @Override
+//            public Message<?> preSend(Message<?> message, MessageChannel channel) {
+//
+//                StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
+//
+//                // (改) 如果是连接请求(CONNECT 命令),从请求头中取出 token 并设置到认证信息中
+//                if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
+//
+//                   // 从连接头中提取授权令牌
+//                   String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION);
+//
+//                   // 验证令牌格式并提取用户信息
+//                   if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) {
+//                       try {
+//                           // 移除 "Bearer " 前缀,从令牌中提取用户信息(username), 并设置到认证信息中
+//                           String tokenWithoutPrefix = bearerToken.substring(7);
+//
+//                           Claims tokenInfo = jwtUtil.extractAllClaims(tokenWithoutPrefix);
+//                           String username = (String) tokenInfo.get("username");
+//
+//                           if (StrUtil.isNotBlank(username)) {
+//                               accessor.setUser(() -> username);
+//                               return message;
+//                           }
+//
+//                       } catch (Exception e) {
+//                           throw new RuntimeException("Failed to process authentication token.");
+//                       }
+//                   }
+//
+//
+//                }
+//                // 不是连接请求,直接放行
+//
+//
+//
+//                return ChannelInterceptor.super.preSend(message, channel);
+//            }
+//        });
+//    }
+//
+//}

+ 718 - 0
src/main/java/com/backendsys/modules/crt/comfyui-api.md

@@ -0,0 +1,718 @@
+---
+title: comfyui-api v1.0.0
+language_tabs:
+  - shell: Shell
+  - http: HTTP
+  - javascript: JavaScript
+  - ruby: Ruby
+  - python: Python
+  - php: PHP
+  - java: Java
+  - go: Go
+toc_footers: []
+includes: []
+search: true
+code_clipboard: true
+highlight_theme: darkula
+headingLevel: 2
+generator: "@tarslib/widdershins v4.0.17"
+
+---
+
+# comfyui-api
+
+> v1.0.0
+
+Base URLs:
+
+# Authentication
+
+# comfyui-api文档
+
+## GET /history
+
+GET /history
+
+获取所有历史任务数据
+
+### 请求参数
+
+|名称|位置|类型|必选|说明|
+|---|---|---|---|---|
+|prompt_id|query|string| 否 |8b918008-751f-414c-9575-7174e841ceac|
+
+> 返回示例
+
+> 200 Response
+
+```json
+{}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|成功|Inline|
+
+### 返回数据结构
+
+## GET /embeddings
+
+GET /embeddings
+
+获取一个列表
+
+> 返回示例
+
+> 200 Response
+
+```json
+{}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|成功|Inline|
+
+### 返回数据结构
+
+## GET /history/{prompt_id}
+
+GET /history/8b918008-751f-414c-9575-7174e841ceac
+
+获取历史任务数据(根据任务id获取历史数据)
+
+> 返回示例
+
+> 成功
+
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|成功|Inline|
+
+### 返回数据结构
+
+## GET /extensions
+
+GET /extensions
+
+获取扩展节点文件列表
+
+> 返回示例
+
+> 200 Response
+
+```json
+{}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|成功|Inline|
+
+### 返回数据结构
+
+## POST /upload/image
+
+POST /upload/image
+
+上传图片接口
+
+> Body 请求参数
+
+```yaml
+image: string
+
+```
+
+### 请求参数
+
+|名称|位置|类型|必选|说明|
+|---|---|---|---|---|
+|body|body|object| 否 |none|
+|» image|body|string(binary)| 是 |图片将以二进制格式发送到服务器|
+
+> 返回示例
+
+> 成功
+
+```json
+{
+  "name": "aaa (7).webp",
+  "subfolder": "",
+  "type": "input"
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|成功|Inline|
+
+### 返回数据结构
+
+## POST /upload/mask
+
+POST /upload/mask
+
+上传蒙版图片接口,一般用于局部重绘
+
+> Body 请求参数
+
+```yaml
+image: string
+type: input
+subfolder: clipspace
+original_ref: "{“filename”:”下载.png”,”type”:”input”,”subfolder”:”clipspace”}"
+
+```
+
+### 请求参数
+
+|名称|位置|类型|必选|说明|
+|---|---|---|---|---|
+|body|body|object| 否 |none|
+|» image|body|string(binary)| 是 |图片将以二进制格式发送到服务器|
+|» type|body|string| 否 |上传图片的目标文件夹|
+|» subfolder|body|string| 否 |上传图片的目标子文件夹|
+|» original_ref|body|string| 是 |无|
+
+> 返回示例
+
+> 成功
+
+```json
+{
+  "name": "下载.png",
+  "subfolder": "clipspace",
+  "type": "input"
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|成功|Inline|
+
+### 返回数据结构
+
+## GET /view
+
+GET /view
+
+图片的在线预览接口(上传图像,生图图像,蒙蔽图像,均通过该接口预览)
+
+### 请求参数
+
+|名称|位置|类型|必选|说明|
+|---|---|---|---|---|
+|filename|query|string| 是 |图片名称|
+|type|query|string| 否 |图片存放位置的文件夹(input为长传图片,output为生成的图片)|
+|subfolder|query|string| 否 |子文件夹(没有可不填)|
+|preview|query|string| 否 |预览|
+|channel|query|string| 否 |无|
+
+> 返回示例
+
+> 成功
+
+```json
+"<img src=\"blob:file:///88efa21f-8f36-4540-aa34-436aa404ce3f\" alt=\"runapi直接显示图片\" />"
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|成功|Inline|
+
+### 返回数据结构
+
+## POST /view_metadata/{folder_name}
+
+POST /view_metadata/{folder_name}
+
+无
+
+### 请求参数
+
+|名称|位置|类型|必选|说明|
+|---|---|---|---|---|
+|folder_name|path|string| 是 |none|
+
+> 返回示例
+
+> 200 Response
+
+```json
+{}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|成功|Inline|
+
+### 返回数据结构
+
+## GET /system_stats
+
+GET /system_stats
+
+系统统计信息接口
+
+> 返回示例
+
+> 成功
+
+```json
+{
+  "system": {
+    "os": "posix",
+    "python_version": "3.10.12 (main, Jun 11 2023, 05:26:28) [GCC 11.4.0]",
+    "embedded_python": false
+  },
+  "devices": [
+    {
+      "name": "cuda:0 NVIDIA GeForce RTX 2080 Ti : cudaMallocAsync",
+      "type": "cuda",
+      "index": 0,
+      "vram_total": 23266590720,
+      "vram_free": 13600655680,
+      "torch_vram_total": 6811549696,
+      "torch_vram_free": 26970432
+    }
+  ]
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|成功|Inline|
+
+### 返回数据结构
+
+## GET /prompt
+
+GET /prompt
+
+获取服务器当前剩余任务列队的数量
+
+> 返回示例
+
+> 成功
+
+```json
+{
+  "exec_info": {
+    "queue_remaining": 1
+  }
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|成功|Inline|
+
+### 返回数据结构
+
+## POST /prompt
+
+POST /prompt
+
+绘图任务的下发接口,此接口只做任务下发,返回任务ID信息。
+此接口只做任务下发,返回任务ID信息。
+
+> Body 请求参数
+
+```json
+{
+  "client_id": "533ef3a3-39c0-4e39-9ced-37d290f371f8",
+  "prompt": {
+    "3": {
+      "inputs": {
+        "seed": 764714814161513,
+        "steps": 26,
+        "cfg": 5,
+        "sampler_name": "dpmpp_3m_sde_gpu",
+        "scheduler": "karras",
+        "denoise": 1,
+        "model": [
+          "40",
+          0
+        ],
+        "positive": [
+          "49",
+          0
+        ],
+        "negative": [
+          "6",
+          0
+        ],
+        "latent_image": [
+          "5",
+          0
+        ]
+      },
+      "class_type": "KSampler"
+    },
+    "5": {
+      "inputs": {
+        "width": 1024,
+        "height": 768,
+        "batch_size": 1
+      },
+      "class_type": "EmptyLatentImage"
+    },
+    "6": {
+      "inputs": {
+        "text": "",
+        "clip": [
+          "40",
+          1
+        ]
+      },
+      "class_type": "CLIPTextEncode"
+    },
+    "8": {
+      "inputs": {
+        "samples": [
+          "3",
+          0
+        ],
+        "vae": [
+          "40",
+          2
+        ]
+      },
+      "class_type": "VAEDecode"
+    },
+    "9": {
+      "inputs": {
+        "filename_prefix": "ComfyUI",
+        "images": [
+          "8",
+          0
+        ]
+      },
+      "class_type": "SaveImage"
+    },
+    "13": {
+      "inputs": {
+        "clip_vision": [
+          "39",
+          0
+        ],
+        "image": [
+          "34",
+          0
+        ]
+      },
+      "class_type": "CLIPVisionEncode"
+    },
+    "19": {
+      "inputs": {
+        "strength": 1,
+        "noise_augmentation": 0,
+        "conditioning": [
+          "42",
+          0
+        ],
+        "clip_vision_output": [
+          "13",
+          0
+        ]
+      },
+      "class_type": "unCLIPConditioning"
+    },
+    "34": {
+      "inputs": {
+        "image": "clipspace/clipspace-mask-1645940.7000000002.png [input]",
+        "choose file to upload": "image"
+      },
+      "class_type": "LoadImage"
+    },
+    "36": {
+      "inputs": {
+        "clip_vision": [
+          "39",
+          0
+        ],
+        "image": [
+          "38",
+          0
+        ]
+      },
+      "class_type": "CLIPVisionEncode"
+    },
+    "37": {
+      "inputs": {
+        "strength": 0.75,
+        "noise_augmentation": 0,
+        "conditioning": [
+          "19",
+          0
+        ],
+        "clip_vision_output": [
+          "36",
+          0
+        ]
+      },
+      "class_type": "unCLIPConditioning"
+    },
+    "38": {
+      "inputs": {
+        "image": "beijing1 (2).webp",
+        "choose file to upload": "image"
+      },
+      "class_type": "LoadImage"
+    },
+    "39": {
+      "inputs": {
+        "clip_name": "clip_vision_g.safetensors"
+      },
+      "class_type": "CLIPVisionLoader"
+    },
+    "40": {
+      "inputs": {
+        "ckpt_name": "sd_xl_base_1.0.safetensors"
+      },
+      "class_type": "CheckpointLoaderSimple"
+    },
+    "42": {
+      "inputs": {
+        "conditioning": [
+          "6",
+          0
+        ]
+      },
+      "class_type": "ConditioningZeroOut"
+    },
+    "43": {
+      "inputs": {
+        "safe": "enable"
+      },
+      "class_type": "HEDPreprocessor"
+    },
+    "44": {
+      "inputs": {
+        "safe": "enable",
+        "image": [
+          "34",
+          0
+        ]
+      },
+      "class_type": "HEDPreprocessor"
+    },
+    "45": {
+      "inputs": {
+        "images": [
+          "44",
+          0
+        ]
+      },
+      "class_type": "PreviewImage"
+    },
+    "46": {
+      "inputs": {
+        "control_net_name": "control-lora-depth-rank256.safetensors"
+      },
+      "class_type": "ControlNetLoader"
+    },
+    "47": {
+      "inputs": {
+        "image": [
+          "34",
+          0
+        ]
+      },
+      "class_type": "ScribblePreprocessor"
+    },
+    "48": {
+      "inputs": {
+        "images": [
+          "47",
+          0
+        ]
+      },
+      "class_type": "PreviewImage"
+    },
+    "49": {
+      "inputs": {
+        "strength": 0.5,
+        "conditioning": [
+          "37",
+          0
+        ],
+        "control_net": [
+          "46",
+          0
+        ],
+        "image": [
+          "47",
+          0
+        ]
+      },
+      "class_type": "ControlNetApply"
+    }
+  }
+}
+```
+
+### 请求参数
+
+|名称|位置|类型|必选|说明|
+|---|---|---|---|---|
+|body|body|object| 否 |none|
+
+> 返回示例
+
+> 成功
+
+```json
+{
+  "prompt_id": "352c1fc4-7382-4c4a-965f-583c4b126a1b",
+  "number": 38,
+  "node_errors": {}
+}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|成功|Inline|
+
+### 返回数据结构
+
+## GET /object_info
+
+GET /object_info
+
+获取系统中所有组件以及可用参数
+
+> 返回示例
+
+> 成功
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|成功|Inline|
+
+### 返回数据结构
+
+## GET /object_info/{node_class}
+
+GET /object_info/KSampler
+
+根据组件名称获取系统中组件参数
+
+> 返回示例
+
+> 成功
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|成功|Inline|
+
+### 返回数据结构
+
+## GET /queue
+
+GET /queue
+
+获取详细任务队列信息,正在运行的以及挂起的
+
+> 返回示例
+
+> 成功
+
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|成功|Inline|
+
+### 返回数据结构
+
+## POST /queue
+
+POST /queue
+
+删除列队/无返回信息则为成功
+
+> Body 请求参数
+
+```json
+{
+  "delete": "string"
+}
+```
+
+### 请求参数
+
+|名称|位置|类型|必选|说明|
+|---|---|---|---|---|
+|body|body|object| 否 |none|
+|» delete|body|string| 是 |包含任务id的列表|
+
+> 返回示例
+
+> 200 Response
+
+```json
+{}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|成功|Inline|
+
+### 返回数据结构
+
+## GET /interrupt
+
+GET /interrupt
+
+取消当前任务/不需任何参数
+
+> 返回示例
+
+> 200 Response
+
+```json
+{}
+```
+
+### 返回结果
+
+|状态码|状态码含义|说明|数据模型|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|成功|Inline|
+
+### 返回数据结构
+
+# 数据模型
+

+ 42 - 0
src/main/java/com/backendsys/modules/crt/controller/CrtGenerateController.java

@@ -0,0 +1,42 @@
+package com.backendsys.modules.crt.controller;
+
+import com.backendsys.modules.common.config.security.annotations.Anonymous;
+import com.backendsys.modules.common.utils.Result;
+import com.backendsys.modules.crt.entity.CrtDramaProjectStoryboard;
+import com.backendsys.modules.crt.service.CrtGenerateService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@Validated
+@RestController
+@Tag(name = "短剧创作-生成")
+public class CrtGenerateController {
+
+    @Autowired
+    private CrtGenerateService crtGenerateService;
+
+    @PreAuthorize("@sr.hasPermission('36.3')")
+    @Operation(summary = "查询任务队列")
+    @GetMapping("/api/crt/generate/getQueue")
+    public Result getQueue() {
+        return Result.success().put("data", crtGenerateService.getQueue());
+    }
+
+    // 生成图片
+    @PreAuthorize("@sr.hasPermission('36.3')")
+    @Operation(summary = "生成图片")
+    @PostMapping("/api/crt/generate/image")
+    public Result generateImage(@Validated(CrtDramaProjectStoryboard.GenerateImage.class) @RequestBody CrtDramaProjectStoryboard crtDramaProjectStoryboard) {
+        return Result.success().put("data", crtGenerateService.generateImage(crtDramaProjectStoryboard));
+    }
+
+    // 生成视频
+
+}

+ 9 - 0
src/main/java/com/backendsys/modules/crt/dao/CrtGenerateImageDao.java

@@ -0,0 +1,9 @@
+package com.backendsys.modules.crt.dao;
+
+import com.backendsys.modules.crt.entity.CrtGenerateImage;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface CrtGenerateImageDao extends BaseMapper<CrtGenerateImage> {
+}

+ 3 - 1
src/main/java/com/backendsys/modules/crt/entity/CrtDramaProjectStoryboard.java

@@ -9,6 +9,7 @@ import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import com.google.gson.annotations.JsonAdapter;
 import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
 import jakarta.validation.constraints.Size;
 import lombok.Data;
@@ -26,12 +27,13 @@ public class CrtDramaProjectStoryboard {
     public static interface Update{}
     public static interface Clear{}
     public static interface Delete{}
+    public static interface GenerateImage{}
 
     @TableId(type = IdType.AUTO)
     private Long id;
 
     @TableField(exist = false)
-    @NotNull(message = "分镜ID不能为空", groups = { Update.class})
+    @NotNull(message = "分镜ID不能为空", groups = { Update.class, GenerateImage.class })
     private Long drama_project_storyboard_id;
 
     private Long user_id;

+ 1 - 1
src/main/java/com/backendsys/modules/crt/entity/CrtDramaTask.java

@@ -12,7 +12,7 @@ public class CrtDramaTask {
     @TableId(type = IdType.AUTO)
     private Long id;
     private Long user_id;                       // 用户ID
-    private Long drama_project_id;          // 项目ID
+    private Long drama_project_id;              // 项目ID
     private String task_id;                     // 任务ID
     private Integer task_type;                  // 任务类型 (1:图像, 2:视频)
     private Integer task_status;                // 任务状态 (-1:未开始, 1:进行中, 2:成功, 3:失败)

+ 27 - 0
src/main/java/com/backendsys/modules/crt/entity/CrtGenerateImage.java

@@ -0,0 +1,27 @@
+package com.backendsys.modules.crt.entity;
+
+import com.backendsys.config.Mybatis.handler.timezone.LocalDateTimeAdapter;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.google.gson.annotations.JsonAdapter;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("crt_generate_image")
+public class CrtGenerateImage {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long user_id;                       // 用户ID
+    private String name;                        // 图片名称
+    private String url_origin;                  // 原图
+    private String url;                          // 转存图
+
+    @JsonAdapter(LocalDateTimeAdapter.class)
+    private LocalDateTime create_time;
+    @JsonAdapter(LocalDateTimeAdapter.class)
+    private LocalDateTime update_time;
+}

+ 15 - 0
src/main/java/com/backendsys/modules/crt/service/CrtGenerateService.java

@@ -0,0 +1,15 @@
+package com.backendsys.modules.crt.service;
+
+import com.backendsys.modules.crt.entity.CrtDramaProjectStoryboard;
+
+import java.util.Map;
+
+public interface CrtGenerateService {
+
+    // [ComfyUI] 查询任务队列
+    Map<String, Object> getQueue();
+
+    // 短剧创作-生成图片
+    Map<String, Object> generateImage(CrtDramaProjectStoryboard crtDramaProjectStoryboard);
+
+}

+ 156 - 0
src/main/java/com/backendsys/modules/crt/service/impl/CrtGenerateServiceImpl.java

@@ -0,0 +1,156 @@
+package com.backendsys.modules.crt.service.impl;
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.backendsys.exception.CustException;
+import com.backendsys.modules.common.utils.Result;
+import com.backendsys.modules.crt.dao.CrtDramaProjectStoryboardDao;
+import com.backendsys.modules.crt.entity.CrtDramaProjectStoryboard;
+import com.backendsys.modules.crt.service.CrtGenerateService;
+import com.backendsys.modules.sdk.comfyui.entity.CFPromptResponse;
+import com.backendsys.modules.sdk.comfyui.entity.CFQueue;
+import com.backendsys.modules.sdk.comfyui.service.ComfyUIService;
+import com.backendsys.modules.sdk.comfyui.service.ComfyUISocketService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Mono;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.UUID;
+
+@Service
+public class CrtGenerateServiceImpl implements CrtGenerateService {
+
+    @Autowired
+    private ComfyUIService comfyUIService;
+    @Autowired
+    private ComfyUISocketService comfyUISocketService;
+    @Autowired
+    private CrtDramaProjectStoryboardDao crtDramaProjectStoryboardDao;
+
+    /**
+     * [ComfyUI] 查询任务队列
+     */
+    @Override
+    public Map<String, Object> getQueue() {
+        // [ComfyUI] 执行任务
+        Mono<CFQueue> cfQueueMono = comfyUIService.getQueue();
+        CFQueue response = cfQueueMono.block();
+        System.out.println("结果: " + response);
+
+        Map<String, Object> resp = new LinkedHashMap<>();
+        resp.put("response", response);
+        return resp;
+    }
+
+    /**
+     * 短剧创作-生成图片
+     */
+    @Override
+    public Map<String, Object> generateImage(CrtDramaProjectStoryboard crtDramaProjectStoryboard) {
+
+        Long drama_project_storyboard_id = crtDramaProjectStoryboard.getDrama_project_storyboard_id();
+
+        CrtDramaProjectStoryboard detail = crtDramaProjectStoryboardDao.selectById(drama_project_storyboard_id);
+        if (detail == null) throw new CustException("分镜不存在");
+
+        // -- 前端生成的UUID ---------------------------------------------
+        String client_id = Convert.toStr(UUID.randomUUID());
+
+        // -- [ComfyUI] 创建 WebSocket 监听连接 ---------------------------
+        comfyUISocketService.connectToSse(client_id, 8001).subscribe();
+
+        // -- [ComfyUI] 执行任务 -----------------------------------------
+
+        // [Demo-基础生图]
+        String prompt = "{" +
+                    "\"3\": {" +
+                        "\"inputs\": {" +
+                            "\"seed\": 449753344472378," +
+                            "\"steps\": 20," +
+                            "\"cfg\": 8," +
+                            "\"sampler_name\": \"euler\"," +
+                            "\"scheduler\": \"normal\"," +
+                            "\"denoise\": 1," +
+                            "\"model\": [\"4\", 0]," +
+                            "\"positive\": [\"6\", 0]," +
+                            "\"negative\": [\"7\", 0]," +
+                            "\"latent_image\": [\"5\", 0]" +
+                        "}," +
+                        "\"class_type\": \"KSampler\"," +
+                        "\"_meta\": { \"title\": \"K采样器\" }" +
+                    "}," +
+                    "\"4\": {" +
+                        "\"inputs\": {" +
+                            "\"ckpt_name\": \"v1-5-pruned-emaonly-fp16.safetensors\"" +
+                        "}," +
+                        "\"class_type\": \"CheckpointLoaderSimple\"," +
+                        "\"_meta\": { \"title\": \"Checkpoint加载器(简易)\" }" +
+                    "}," +
+                    "\"5\": {" +
+                        "\"inputs\": {" +
+                            "\"width\": 512, \"height\": 512, \"batch_size\": 1" +
+                        "}," +
+                        "\"class_type\": \"EmptyLatentImage\"," +
+                        "\"_meta\": { \"title\": \"空Latent图像\" }" +
+                    "}," +
+                    "\"6\": {" +
+                        "\"inputs\": {" +
+                            "\"text\": \"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,\"," +
+                            "\"speak_and_recognation\": {" +
+                                "\"__value__\": [false, true]" +
+                            "}," +
+                            "\"clip\": [\"4\", 1]" +
+                        "}," +
+                        "\"class_type\": \"CLIPTextEncode\"," +
+                        "\"_meta\": { \"title\": \"CLIP文本编码\" }" +
+                    "}," +
+                    "\"7\": {" +
+                        "\"inputs\": {" +
+                            "\"text\": \"text, watermark\"," +
+                            "\"speak_and_recognation\": {" +
+                                "\"__value__\": [false, true]" +
+                            "}," +
+                            "\"clip\": [\"4\", 1]" +
+                        "}," +
+                        "\"class_type\": \"CLIPTextEncode\"," +
+                        "\"_meta\": { \"title\": \"CLIP文本编码\" }" +
+                    "}," +
+                    "\"8\": {" +
+                        "\"inputs\": {" +
+                            "\"samples\": [\"3\", 0]," +
+                            "\"vae\": [\"4\", 2]," +
+                        "}," +
+                        "\"class_type\": \"VAEDecode\"," +
+                        "\"_meta\": { \"title\": \"VAE解码\" }" +
+                    "}," +
+                    "\"9\": {" +
+                        "\"inputs\": {" +
+                            "\"filename_prefix\": \"ComfyUI\"," +
+                            "\"images\": [\"8\", 0]" +
+                        "}," +
+                        "\"class_type\": \"SaveImage\"," +
+                        "\"_meta\": { \"title\": \"保存图像\" }" +
+                    "}," +
+                "}";
+
+        JSONObject prompt_object = JSONUtil.parseObj(prompt);
+
+        System.out.println("prompt_object: " + prompt_object);
+
+        // [ComfyUI] 执行任务
+        Mono<CFPromptResponse> cfPromptResponseMono = comfyUIService.prompt(client_id, prompt_object);
+        CFPromptResponse response = cfPromptResponseMono.block();
+        response.setClient_id(client_id);
+        System.out.println("结果: " + response);
+
+        Map<String, Object> resp = new LinkedHashMap<>();
+        resp.put("drama_project_storyboard_id", drama_project_storyboard_id);
+        resp.put("response", response);
+        return resp;
+    }
+
+}

+ 0 - 14
src/main/java/com/backendsys/modules/sdk/baidu/bce/utils/BaiduBceUtil.java

@@ -3,23 +3,9 @@ package com.backendsys.modules.sdk.baidu.bce.utils;
 import cn.hutool.http.HttpUtil;
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
-import org.apache.http.client.ClientProtocolException;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.impl.client.HttpClients;
-import org.apache.http.util.EntityUtils;
 import org.springframework.beans.factory.annotation.Value;
-import org.springframework.core.ParameterizedTypeReference;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.MediaType;
 import org.springframework.stereotype.Component;
-import org.springframework.web.reactive.function.client.WebClient;
-import org.springframework.web.util.UriComponentsBuilder;
 
-import java.io.IOException;
-import java.net.URI;
 import java.util.HashMap;
 import java.util.Map;
 

+ 50 - 0
src/main/java/com/backendsys/modules/sdk/comfyui/controller/ComfyUIDemoController.java

@@ -0,0 +1,50 @@
+package com.backendsys.modules.sdk.comfyui.controller;
+
+import com.backendsys.exception.CustException;
+import com.backendsys.modules.common.config.security.annotations.Anonymous;
+import com.backendsys.modules.sdk.comfyui.service.ComfyUIService;
+import com.backendsys.modules.sdk.comfyui.service.ComfyUISocketService;
+import com.tencentcloudapi.tione.v20211111.models.ChatCompletionResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+import reactor.core.publisher.Flux;
+
+import java.io.IOException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+@RestController
+public class ComfyUIDemoController {
+
+    @Autowired
+    private ComfyUIService comfyUIService;
+
+    @Autowired
+    private ComfyUISocketService comfyUISocketService;
+
+    /**
+     * [ComfyUI] 创建 WebSocket 监听连接
+     */
+    @Anonymous
+    @PostMapping("/api/comfyui/ws/connect")
+    public String connect(String clientId) {
+        comfyUISocketService.connect(clientId, 8001).subscribe();
+        return "Connection initiated for: " + clientId;
+    }
+
+    /**
+     * [ComfyUI] 断开 WebSocket 监听连接
+     */
+    @Anonymous
+    @PostMapping("/api/comfyui/ws/disconnect")
+    public String disconnect(String clientId) {
+        comfyUISocketService.disconnect(clientId).subscribe();
+        return "Disconnected: " + clientId;
+    }
+
+}

+ 12 - 0
src/main/java/com/backendsys/modules/sdk/comfyui/entity/CFPromptRequest.java

@@ -0,0 +1,12 @@
+package com.backendsys.modules.sdk.comfyui.entity;
+
+import cn.hutool.json.JSONObject;
+import lombok.Data;
+
+@Data
+public class CFPromptRequest {
+
+    private String client_id;
+    private JSONObject prompt;
+
+}

+ 13 - 0
src/main/java/com/backendsys/modules/sdk/comfyui/entity/CFPromptResponse.java

@@ -0,0 +1,13 @@
+package com.backendsys.modules.sdk.comfyui.entity;
+
+import lombok.Data;
+
+@Data
+public class CFPromptResponse {
+    private String client_id;       // 任务ID
+    private String prompt_id;       // 任务ID
+    private Integer number;         // 当前任务序号,可用于后续获取需要等待任务数的计算
+    private Object node_errors;     // 错误信息
+    private Object error;
+
+}

+ 10 - 0
src/main/java/com/backendsys/modules/sdk/comfyui/entity/CFQueue.java

@@ -0,0 +1,10 @@
+package com.backendsys.modules.sdk.comfyui.entity;
+
+import cn.hutool.json.JSONArray;
+import lombok.Data;
+
+@Data
+public class CFQueue {
+    private JSONArray queue_running;
+    private JSONArray queue_pending;
+}

+ 24 - 0
src/main/java/com/backendsys/modules/sdk/comfyui/enums/TypeEnums.java

@@ -0,0 +1,24 @@
+package com.backendsys.modules.sdk.comfyui.enums;
+
+public enum TypeEnums {
+
+    STATUS("status", "队列状态"),
+    EXECUTION_START("execution_start", "任务开始执行"),
+    EXECUTION_CACHED("execution_cached", "任务缓存"),
+    EXECUTING("executing", "当前任务执行的步骤"),
+    EXECUTION_SUCCESS("execution_success", "任务执行成功"),
+    EXECUTED("executed", "任务执行完成");
+
+    private final String value;
+    private final String label;
+    TypeEnums(String value, String label) {
+        this.value = value;
+        this.label = label;
+    }
+    public String getLabel() {
+        return this.label;
+    }
+    public String getValue() {
+        return this.value;
+    }
+}

+ 16 - 0
src/main/java/com/backendsys/modules/sdk/comfyui/service/ComfyUIService.java

@@ -0,0 +1,16 @@
+package com.backendsys.modules.sdk.comfyui.service;
+
+import cn.hutool.json.JSONObject;
+import com.backendsys.modules.sdk.comfyui.entity.CFPromptResponse;
+import com.backendsys.modules.sdk.comfyui.entity.CFQueue;
+import reactor.core.publisher.Mono;
+
+public interface ComfyUIService {
+
+    // [ComfyUI] 查询任务队列
+    Mono<CFQueue> getQueue();
+
+    // [ComfyUI] 执行任务
+    Mono<CFPromptResponse> prompt(String client_id, JSONObject prompt);
+
+}

+ 16 - 0
src/main/java/com/backendsys/modules/sdk/comfyui/service/ComfyUISocketService.java

@@ -0,0 +1,16 @@
+package com.backendsys.modules.sdk.comfyui.service;
+
+import reactor.core.publisher.Mono;
+
+public interface ComfyUISocketService {
+
+    // [ComfyUI] 创建 WebSocket 监听连接
+    Mono<Void> connect(String clientId, Integer port);
+
+    // [ComfyUI] 创建 WebSocket 监听连接 (转发到 SSE)
+    Mono<Void> connectToSse(String clientId, Integer port);
+
+    // [ComfyUI] 断开 WebSocket 监听连接
+    Mono<Void> disconnect(String clientId);
+
+}

+ 93 - 0
src/main/java/com/backendsys/modules/sdk/comfyui/service/impl/ComfyUIServiceImpl.java

@@ -0,0 +1,93 @@
+package com.backendsys.modules.sdk.comfyui.service.impl;
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.json.JSONObject;
+import com.backendsys.modules.common.Filter.WebClientFilter;
+import com.backendsys.modules.sdk.comfyui.entity.CFPromptRequest;
+import com.backendsys.modules.sdk.comfyui.entity.CFPromptResponse;
+import com.backendsys.modules.sdk.comfyui.entity.CFQueue;
+import com.backendsys.modules.sdk.comfyui.service.ComfyUIService;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Service;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.util.UriComponentsBuilder;
+import reactor.core.publisher.Mono;
+
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.Consumer;
+
+@Service
+public class ComfyUIServiceImpl implements ComfyUIService {
+
+    @Value("${comfyui.host}")
+    private String COMFYUI_HOST;
+    @Value("${comfyui.token}")
+    private String COMFYUI_TOKEN;
+
+    private WebClient webClient;
+    public WebClient getWebClient() {
+        if (webClient == null) {
+//            webClient = WebClient.builder().baseUrl(COMFYUI_HOST + ":8001").filter(WebClientFilter.logFilter).build();
+            webClient = WebClient.builder().filter(WebClientFilter.logFilter).build();
+        }
+        return webClient;
+    }
+
+    /**
+     * 获取 Token 拼接好的 HttpHeaders
+     */
+    public Consumer<HttpHeaders> getHttpHeaders() {
+        Consumer<HttpHeaders> httpHeaders = (headers) -> {
+            headers.add("Authorization", "Bearer " + COMFYUI_TOKEN);
+        };
+        return httpHeaders;
+    }
+
+
+    /**
+     * [ComfyUI] 查询任务队列
+     */
+    public Mono<CFQueue> getQueue() {
+        String url = "http://" + COMFYUI_HOST + ":8001/queue";
+        WebClient webClient = getWebClient();
+        return webClient.get()
+                .uri(url)
+                .headers(getHttpHeaders())
+                .accept(MediaType.APPLICATION_JSON)
+                .exchangeToMono(response -> response.bodyToMono(CFQueue.class));
+    }
+
+    /**
+     * [ComfyUI] 执行任务
+     */
+    @Override
+    public Mono<CFPromptResponse> prompt(String client_id, JSONObject prompt) {
+
+        CFPromptRequest bodyValue = new CFPromptRequest();
+        bodyValue.setClient_id(client_id);
+        bodyValue.setPrompt(prompt);
+
+        String url = "http://" + COMFYUI_HOST + ":8001/prompt";
+
+        WebClient webClient = getWebClient();
+        return webClient.post()
+                .uri(url)
+                .headers(getHttpHeaders())
+                .accept(MediaType.APPLICATION_JSON)
+                .bodyValue(bodyValue)
+                .exchangeToMono(response -> response.bodyToMono(CFPromptResponse.class))
+                .onErrorResume(e -> {
+                    // 捕获所有异常(包括上面抛出的 RuntimeException)
+                    CFPromptResponse response = new CFPromptResponse();
+                    response.setNode_errors(e.getMessage());
+                    return Mono.just(response);
+                });
+
+    }
+
+}

+ 213 - 0
src/main/java/com/backendsys/modules/sdk/comfyui/service/impl/ComfyUISocketServiceImpl.java

@@ -0,0 +1,213 @@
+package com.backendsys.modules.sdk.comfyui.service.impl;
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.json.JSONArray;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.backendsys.modules.ai.chat.entity.ChatSseMessage;
+import com.backendsys.modules.common.config.security.utils.SecurityUtil;
+import com.backendsys.modules.sdk.comfyui.enums.TypeEnums;
+import com.backendsys.modules.sdk.comfyui.service.ComfyUISocketService;
+import com.backendsys.modules.sse.entity.SseResponse;
+import com.backendsys.modules.sse.entity.SseResponseEnum;
+import com.backendsys.modules.sse.utils.SseUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.web.reactive.socket.WebSocketMessage;
+import org.springframework.web.reactive.socket.WebSocketSession;
+import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
+import org.springframework.web.reactive.socket.client.WebSocketClient;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.netty.http.client.HttpClient;
+
+import java.net.URI;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Service
+public class ComfyUISocketServiceImpl implements ComfyUISocketService {
+
+    @Autowired
+    private SseUtil sseUtil;
+
+    @Value("${comfyui.host}")
+    private String COMFYUI_HOST;
+    @Value("${comfyui.token}")
+    private String COMFYUI_TOKEN;
+
+    /**
+     * 管理多个连接
+     */
+    private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
+
+    /**
+     * 创建带有认证头的 WebSocketClient
+     * @param token 认证令牌
+     * @return 配置好的 WebSocketClient
+     */
+    private WebSocketClient createWebSocketClientWithToken(String token) {
+        HttpClient httpClient = HttpClient.create()
+            .headers(headers -> headers.add("Authorization", "Bearer " + token))
+            .responseTimeout(Duration.ofSeconds(30));  // 30秒超时
+        return new ReactorNettyWebSocketClient(httpClient);
+    }
+
+    /**
+     * [ComfyUI] 创建 WebSocket 监听连接
+     */
+    @Override
+    public Mono<Void> connect(String clientId, Integer port) {
+
+        String wsUrl =  "ws://" + COMFYUI_HOST + ":" + port + "/ws";
+        return Mono.defer(() -> {
+            if (sessions.containsKey(clientId)) {
+                return Mono.error(new IllegalStateException("Connection already exists for client: " + clientId));
+            }
+            // 动态创建带有认证头的客户端
+            WebSocketClient clientWithAuth = createWebSocketClientWithToken(COMFYUI_TOKEN);
+            return clientWithAuth.execute(URI.create(wsUrl + "?clientId=" + clientId), session -> {
+                // 保存会话
+                sessions.put(clientId, session);
+                // 接收消息
+                Flux<String> incomingMessages = session.receive()
+                    .map(WebSocketMessage::getPayloadAsText)
+                    .doOnNext(message -> {
+                        System.out.println("(doOnNext) Received from " + clientId + ": " + message);
+                    })
+                    .doOnError(e -> {
+                        System.err.println("(doOnError) Error for " + clientId + ": " + e.getMessage());
+                    })
+                    .doFinally(signal -> {
+                        System.out.println("(doFinally) Connection closed for " + clientId + ": " + signal);
+                        sessions.remove(clientId);
+                    });
+                // 需要返回一个Mono<Void>来表示处理完成
+                return incomingMessages.then();
+            });
+        });
+    }
+
+    /**
+     * [ComfyUI] 创建 WebSocket 监听连接 (转发到 SSE)
+     */
+    @Override
+    public Mono<Void> connectToSse(String clientId, Integer port) {
+
+        Long user_id = SecurityUtil.getUserId();
+
+        String wsUrl =  "ws://" + COMFYUI_HOST + ":" + port + "/ws";
+        return Mono.defer(() -> {
+            if (sessions.containsKey(clientId)) {
+                return Mono.error(new IllegalStateException("Connection already exists for client: " + clientId));
+            }
+            // 动态创建带有认证头的客户端
+            WebSocketClient clientWithAuth = createWebSocketClientWithToken(COMFYUI_TOKEN);
+            return clientWithAuth.execute(URI.create(wsUrl + "?clientId=" + clientId), session -> {
+                // 保存会话
+                sessions.put(clientId, session);
+                // 接收消息
+
+                System.out.println("------ wsUrl: " + wsUrl + " ------");
+                System.out.println("------ connectToSse clientId: " + clientId + ", user_id: " + user_id + " ------");
+
+                Flux<String> incomingMessages = session.receive()
+                        .map(WebSocketMessage::getPayloadAsText)
+                        .doOnNext(message -> {
+                            System.out.println("(doOnNext) Received from " + clientId + ": " + message);
+
+                            JSONObject data = JSONUtil.parseObj(message);
+                            String type = Convert.toStr(data.get("type"));
+
+                            // == [任务执行成功] =======================================================
+                            // { "type": "executed", "data": { "output": { "images": [{ "filename": "ComfyUI_00117_.png" }] } } }
+                            if (TypeEnums.EXECUTED.getValue().equals(type)) {
+
+                                JSONObject dataChildren = JSONUtil.parseObj(data.get("data"));
+                                JSONObject output = JSONUtil.parseObj(dataChildren.get("output"));
+
+                                // -- [生成图片] ------------------------------------------------------
+                                Object imagesObj = output.get("images");
+                                if (imagesObj != null) {
+                                    JSONArray images = JSONUtil.parseArray(imagesObj);
+                                    // [{"filename": "ComfyUI_00122_.png", "subfolder": "", "type": "output"}]
+                                    // http://43.128.1.201:8001/api/view?filename=ComfyUI_00117_.png
+                                    // http://43.128.1.201:8001/api/view?filename=ComfyUI_00117_.png&preview=1
+                                    List<String> images_path = new ArrayList<>();
+                                    if (images.size() > 0) {
+                                        for (int i = 0; i < images.size(); i++) {
+                                            JSONObject image = images.getJSONObject(i);
+                                            String filename = image.getStr("filename");
+                                            String filepath = "http://" + COMFYUI_HOST + ":" + port + "/api/view?filename=" + filename;
+                                            images_path.add(filepath);
+
+                                            // -- [图片转存储存桶] -------------------------------------
+
+                                            // ------------------------------------------------------
+
+                                        }
+                                    }
+                                    output.put("images_path", images_path);
+                                    dataChildren.put("output", output);
+                                    data.put("data", dataChildren);
+                                }
+                                // ------------------------------------------------------------------
+
+                            }
+                            // ======================================================================
+
+                            sseUtil.send(user_id, new SseResponse(SseResponseEnum.COMFYUI, data).toJsonStr());
+                        })
+                        .doOnError(e -> {
+                            System.err.println("(doOnError) Error for " + clientId + ": " + e.getMessage());
+                            sseUtil.send(user_id, new SseResponse(SseResponseEnum.COMFYUI, e.getMessage()).toJsonStr());
+                        })
+                        .doFinally(signal -> {
+                            System.out.println("(doFinally) Connection closed for " + clientId + ": " + signal);
+                            sseUtil.send(user_id, new SseResponse(SseResponseEnum.COMFYUI, signal).toJsonStr());
+                            sessions.remove(clientId);
+                            System.out.println("---------------------------------------------------------");
+                        });
+                // 需要返回一个Mono<Void>来表示处理完成
+                return incomingMessages.then();
+            });
+        });
+    }
+
+    /**
+     * [ComfyUI] 断开 WebSocket 监听连接
+     * @param clientId 客户端ID
+     * @return Mono<Void> 表示断开操作
+     */
+    @Override
+    public Mono<Void> disconnect(String clientId) {
+        return Mono.fromRunnable(() -> {
+            WebSocketSession session = sessions.get(clientId);
+            if (session != null) {
+                System.out.println("disconnect success! clientId: " + clientId);
+
+                Long user_id = SecurityUtil.getUserId();
+                if (user_id != null) {
+                    sseUtil.send(user_id, new SseResponse(SseResponseEnum.COMFYUI, "disconnect success! clientId: " + clientId).toJsonStr());
+                }
+
+                session.close().subscribe();
+                sessions.remove(clientId);
+            }
+        });
+    }
+
+    /**
+     * 获取所有活动的连接ID
+     * @return 连接ID集合
+     */
+    public Flux<String> getActiveConnections() {
+        return Flux.fromIterable(sessions.keySet());
+    }
+
+}

+ 8 - 0
src/main/java/com/backendsys/modules/sdk/comfyui/utils/ComfyUtil.java

@@ -0,0 +1,8 @@
+package com.backendsys.modules.sdk.comfyui.utils;
+
+import org.springframework.stereotype.Component;
+
+@Component
+public class ComfyUtil {
+
+}

+ 3 - 1
src/main/java/com/backendsys/modules/sdk/deepseek/service/impl/DeepSeekClientImpl.java

@@ -21,12 +21,14 @@ import com.backendsys.modules.sse.entity.SseResponseEnum;
 import com.backendsys.modules.sse.utils.SseUtil;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpGet;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.entity.StringEntity;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.http.impl.client.HttpClients;
+
 import org.apache.http.util.EntityUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;

+ 127 - 220
src/main/java/com/backendsys/modules/sdk/deepseek/utils/OllamaUtil.java

@@ -1,10 +1,7 @@
 package com.backendsys.modules.sdk.deepseek.utils;
 
-import cn.hutool.core.convert.Convert;
-import cn.hutool.core.util.NumberUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
-import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSONObject;
 import com.backendsys.modules.ai.chat.entity.Chat;
 import com.backendsys.modules.ai.chat.entity.ChatCompletionParam;
@@ -12,7 +9,6 @@ import com.backendsys.modules.ai.chat.entity.ChatResult;
 import com.backendsys.modules.ai.chat.entity.ChatSseMessage;
 import com.backendsys.modules.common.config.redis.utils.RedisUtil;
 import com.backendsys.modules.sdk.bocha.entity.BochaParam;
-import com.backendsys.modules.sdk.bocha.entity.BochaResult;
 import com.backendsys.modules.sdk.bocha.service.BochaService;
 import com.backendsys.modules.sdk.deepseek.entity.DSRequest;
 import com.backendsys.modules.sdk.deepseek.entity.DSRequestMessage;
@@ -21,20 +17,22 @@ import com.backendsys.modules.sse.entity.SseResponseEnum;
 import com.backendsys.modules.sse.utils.SseUtil;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.entity.StringEntity;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.impl.client.HttpClients;
+//import org.apache.http.client.methods.CloseableHttpResponse;
+//import org.apache.http.impl.client.CloseableHttpClient;
+//import org.apache.http.impl.client.HttpClients;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 
-import java.io.BufferedReader;
-import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
 import java.nio.charset.StandardCharsets;
+import java.time.Duration;
 import java.util.*;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * ollama run deepseek-r1:1.5b
@@ -72,7 +70,7 @@ public class OllamaUtil {
 
         // 定义作用于全局的变量
         Long contentDuration = 0L;
-        Boolean isThinking = false;
+        AtomicReference<Boolean> isThinking = new AtomicReference<>(false);
         StringBuilder allReplyContent = new StringBuilder();
         StringBuilder allThinkContent = new StringBuilder();
         String requestOfRedisKey = APPLICATION_NAME + "-chat-history-" + history_code;
@@ -134,246 +132,155 @@ public class OllamaUtil {
             });
             System.out.println("---------------------------------------------------------------------");
 
-            ObjectMapper objectMapper = new ObjectMapper();
-            try (CloseableHttpClient client = HttpClients.createDefault()) {
 
-                /*
-                【/api/generate】
-                它是一个相对基础的文本生成端点,主要用于根据给定的提示信息生成一段连续的文本。
-                这个端点会基于输入的提示,按照模型的语言生成能力输出一段完整的内容,更侧重于单纯的文本生成任务。
-                生成过程不依赖于上下文的历史对话信息,每次请求都是独立的,模型仅依据当前输入的提示进行文本生成。
-                {
-                    "model": "llama2", "prompt": "请描述一下美丽的海滩", "num_predict": 200, "temperature": 0.7
-                }
 
-                【/api/chat】
-                该端点专为模拟聊天场景设计,具备处理对话上下文的能力。它可以跟踪对话的历史记录,理解对话的上下文信息,从而生成更符合对话逻辑和连贯性的回复。
-                更注重模拟真实的人机对话交互,能够根据历史对话和当前输入生成合适的回应,适用于构建聊天机器人等交互式应用。
-                {
-                    "model": "deepseek-r1:1.5b",
-                    "messages": [
-                        {
-                            "role": "system",
-                            "content": "你是一个能够理解中文指令并帮助完成任务的智能助手。你的任务是根据用户的需求生成合适的分类任务或生成任务,并准确判断这些任务的类型。请确保你的回答简洁、准确且符合中英文语境。"
-                        },
-                        {
-                            // "role": "assistant",
-                            "role": "user",
-                            "content": "写一个简单的 Python 函数,用于计算两个数的和"
-                        }
-                    ],
-                    "stream": false,
-
-                    // 新增
-                    "context": ["引用1","引用2"]
-                }
-                 */
+            /*
+            【/api/generate】
+            它是一个相对基础的文本生成端点,主要用于根据给定的提示信息生成一段连续的文本。
+            这个端点会基于输入的提示,按照模型的语言生成能力输出一段完整的内容,更侧重于单纯的文本生成任务。
+            生成过程不依赖于上下文的历史对话信息,每次请求都是独立的,模型仅依据当前输入的提示进行文本生成。
+            {
+                "model": "llama2", "prompt": "请描述一下美丽的海滩", "num_predict": 200, "temperature": 0.7
+            }
+
+            【/api/chat】
+            该端点专为模拟聊天场景设计,具备处理对话上下文的能力。它可以跟踪对话的历史记录,理解对话的上下文信息,从而生成更符合对话逻辑和连贯性的回复。
+            更注重模拟真实的人机对话交互,能够根据历史对话和当前输入生成合适的回应,适用于构建聊天机器人等交互式应用。
+            {
+                "model": "deepseek-r1:1.5b",
+                "messages": [
+                    { "role": "system", "content": "你是一个能够理解中文指令并帮助完成任务的智能助手。" },
+                    { "role": "user", "content": "写一个简单的 Python 函数,用于计算两个数的和" }
+                ],
+                "stream": false,
+                // 新增
+                "context": ["引用1","引用2"]
+            }
+             */
+
+            try {
 
+                ObjectMapper objectMapper = new ObjectMapper();
+                HttpClient httpClient = HttpClient.newBuilder()
+                    .connectTimeout(Duration.ofSeconds(30))
+                    .build();
 
-                // [Chat] 构建请求体
-                HttpPost request = new HttpPost(DOMAIN + "/api/chat");
                 DSRequest body = new DSRequest();
                 body.setModel(model);
                 body.setMessages(messages);
                 body.setStream(true);
+
                 String requestBody = objectMapper.writeValueAsString(body);
 
-//                // [Generate] 构建请求体
-//                HttpPost request = new HttpPost(DOMAIN + "/api/generate");
-//                Map<String, Object> requestMap = new HashMap<>();
-//                requestMap.put("model", model);
-//                requestMap.put("prompt", prompt);
-//                requestMap.put("stream", true);
-//                String requestBody = objectMapper.writeValueAsString(requestMap);
-
-                request.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8));
-
-                try (CloseableHttpResponse response = client.execute(request);
-                     BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8))) {
-
-                    long apiDuration = System.currentTimeMillis() - allStartTime;   // 接口耗时
-
-                    long thinkStartTime = 0L;                            // 开始思考时间
-                    long thinkDuration = 0L;                             // 思考耗时
-
-                    System.out.println("API 调用耗时: " + apiDuration + " 毫秒");
-                    System.out.println("---- 开始流式回答: ------------------------------------");
-
-                    // [SSE] 发送消息
-                    ChatSseMessage chatLoadingSseMessage = new ChatSseMessage("LOADING", "正在思考", null, history_code);
-                    sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, chatLoadingSseMessage).toJsonStr());
-
-                    String line;
-                    while ((line = reader.readLine()) != null) {
-
-                        // 判断是否中止
-                        if (ObjectUtil.isEmpty(redisUtil.getCacheObject(requestOfRedisKey))) {
-                            System.out.println("中止!");
-                            request.abort();
-                            // 流程结束后,删除锁
-                            redisUtil.delete(requestOfRedisKey);
-                            break;
-                        }
-
-
-                        // System.out.println(line);
-                        /*
-                            ---------------------- [Chat] line ----------------------
-                            {"model":"deepseek-r1:1.5b","created_at":"2025-03-18T07:37:06.483163789Z","message":{"role":"assistant","content":"\u003cthink\u003e"},"done":false}
-
-                            ---------------------- [Generate] line ------------------
-                            {"model":"deepseek-r1:1.5b","created_at":"2025-03-05T10:51:17.443189986Z","response":"\u003cthink\u003e","done":false}
-                            {"model":"deepseek-r1:1.5b","created_at":"2025-03-06T11:08:30.9219611Z","response":"\n\n","done":false}
-
-                            ---------------------- [Error] line ---------------------
-                            {"error":"llama runner process has terminated: error loading model: unable to allocate CUDA0 buffer"}
-                         */
-
-                        // 每行数据可以是一个JSON对象,根据实际情况处理
-                        JSONObject resJson = JSONObject.parseObject(line);
-
-                        // -- 判断内容是否为空 (或报错) --------------------------------------
-                        if (resJson == null) return null;
-                        String errJsonMessage = resJson.getString("error");
-                        if (errJsonMessage != null) {
-                            // [SSE] 发送消息
-                            sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, new ChatSseMessage("REPLY", errJsonMessage, contentDuration, history_code)).toJsonStr());
-                            // [SSE] 发送消息 (完成)
-                            sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, new ChatSseMessage("REPLY", "[DONE][REPLY]", contentDuration, history_code)).toJsonStr());
-                            //
-                            chatResult.setContent(errJsonMessage);
-                            return chatResult;
-                        }
-
-                        // --------------------------------------------------------------
-                        // [Chat]
-                        JSONObject resJsonMessage = resJson.getJSONObject("message");
-                        String content = resJsonMessage.getString("content");
-
-//                        // [Generate]
-//                        String content = resJson.getString("response");
-                        // --------------------------------------------------------------
-
-                        // 开始思考
-                        if (content.contains("<think>")) {
-                            isThinking = true;
-                            thinkStartTime = System.currentTimeMillis();
-                        }
-                        // 停止思考,并计算思考耗时
-                        if (content.contains("</think>")) {
-                            isThinking = false;
-                            thinkDuration = thinkStartTime - allStartTime;
-                            System.out.println("推理耗时: " + thinkDuration + "毫秒");
-                            System.out.println("-----------------------------------------------------");
-
-                            if (allThinkContent.length() > 0){
-                                // [SSE] 发送消息
-                                ChatSseMessage chatSseMessage = new ChatSseMessage("THINK", "[DONE][THINK]", thinkDuration, history_code);
-                                sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, chatSseMessage).toJsonStr());
-                            }
+                HttpRequest request = HttpRequest.newBuilder()
+                    .uri(URI.create(DOMAIN + "/api/chat"))
+                    .header("Content-Type", "application/json")
+                    .POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8))
+                    .build();
 
-                        }
+                long apiDuration = System.currentTimeMillis() - allStartTime;   // 接口耗时
+                System.out.println("API 调用耗时: " + apiDuration + " 毫秒");
+                System.out.println("---- 开始流式回答: ------------------------------------");
 
-                        // [思考] Think
-                        if (isThinking) {
-                            if (!content.contains("<think>") && !content.contains("\n\n") && !content.contains("\n")) {
-                                // [SSE] 发送消息
-                                ChatSseMessage chatSseMessage = new ChatSseMessage("THINK", content, null, history_code);
-                                sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, chatSseMessage).toJsonStr());
-                                // 收集推理内容
-                                allThinkContent.append(content);
+                // [SSE] 发送消息
+                ChatSseMessage chatLoadingSseMessage = new ChatSseMessage("LOADING", "正在思考", null, history_code);
+                sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, chatLoadingSseMessage).toJsonStr());
+
+                // 使用异步流式处理
+                Long finalContentDuration = contentDuration;
+                CompletableFuture<Void> future = httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofLines())
+                    .thenAccept(resp -> {
+                        resp.body().forEach(line -> {
+                            if (ObjectUtil.isEmpty(redisUtil.getCacheObject(requestOfRedisKey))) {
+                                System.out.println("中止!");
+                                redisUtil.delete(requestOfRedisKey);
+                                return;
+                            }
+
+                            JSONObject resJson = JSONObject.parseObject(line);
+                            if (resJson == null) return;
+
+                            String errJsonMessage = resJson.getString("error");
+                            if (errJsonMessage != null) {
+                                ChatSseMessage errMsg = new ChatSseMessage("REPLY", errJsonMessage, finalContentDuration, history_code);
+                                sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, errMsg).toJsonStr());
+                                sseUtil.send(user_id,
+                                        new SseResponse(SseResponseEnum.OLLAMA,
+                                                new ChatSseMessage("REPLY", "[DONE][REPLY]", finalContentDuration, history_code)).toJsonStr());
+                                chatResult.setContent(errJsonMessage);
+                                return;
                             }
-                        }
 
-                        // [回答] Reply
-                        if (!isThinking) {
+                            JSONObject resJsonMessage = resJson.getJSONObject("message");
+                            String content = resJsonMessage.getString("content");
 
-                            // System.out.println("content: " + content);
+                            // 思考标记
+                            if (content.contains("<think>")) {
+                                isThinking.set(true);
+                            }
+                            if (content.contains("</think>")) {
+                                isThinking.set(false);
+                                long thinkDuration = System.currentTimeMillis() - allStartTime;
+                                if (allThinkContent.length() > 0) {
+                                    ChatSseMessage thinkMsg = new ChatSseMessage("THINK", "[DONE][THINK]", thinkDuration, history_code);
+                                    sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, thinkMsg).toJsonStr());
+                                }
+                            }
 
-                            if (!content.contains("</think>") && !content.contains("\n\n")) {
+                            if (isThinking.get() && !content.contains("<think>") && !content.equals("\n\n") && !content.equals("\n")) {
+                                ChatSseMessage thinkMsg = new ChatSseMessage("THINK", content, null, history_code);
+                                sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, thinkMsg).toJsonStr());
+                                allThinkContent.append(content);
+                            }
 
+                            if (!isThinking.get() && !content.contains("</think>") && !content.equals("\n\n")) {
                                 Boolean done = resJson.getBoolean("done");
                                 if (!done) {
-
-                                    // [SSE] 发送消息
-                                    ChatSseMessage chatSseMessage = new ChatSseMessage("REPLY", content, null, history_code);
-                                    sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, chatSseMessage).toJsonStr());
-                                    // 收集回答内容
+                                    ChatSseMessage replyMsg = new ChatSseMessage("REPLY", content, null, history_code);
+                                    sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, replyMsg).toJsonStr());
                                     allReplyContent.append(content);
-
                                 }
                             }
-                        }
-
-                    }
-
+                        });
+                    });
 
-                    System.out.println("-------------------- 结束流式回答. --------------------");
-                    contentDuration = System.currentTimeMillis() - allStartTime;
+                future.join(); // 阻塞直至流结束
 
-                    System.out.println("全部推理: " + allThinkContent);
-                    System.out.println("全部回答: " + allReplyContent);
-                    System.out.println("总输出耗时: " + contentDuration + " 毫秒");
-                    System.out.println("---------------------------------------------------");
-
-                    if (StrUtil.isNotEmpty(allThinkContent.toString())) {
-                        chatResult.setReasoning_content(allThinkContent.toString());
-                        chatResult.setReasoning_duration(thinkDuration);
-                    }
-                    chatResult.setContent(allReplyContent.toString());
-                    chatResult.setContent_duration(contentDuration);
-                    return chatResult;
-
-                } catch (Exception e) {
-                    System.out.println("Exception(1): " + e.getMessage());
-                    String message = e.getMessage();
-                    if (message.contains("failed to respond")) {
-                        message = "(系统繁忙,请稍后再试)";
-                    }
-                    if (message.contains("Premature end of chunk coded message body: closing chunk expected")) {
-                        message = "(请求中止)";
-                    }
-                    // [SSE] 发送消息
-                    String contentType = (isThinking ? "THINK_ABORT" : "REPLY_ABORT");
-                    ChatSseMessage chatSseMessage = new ChatSseMessage(contentType, message, contentDuration, history_code);
-                    sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, chatSseMessage).toJsonStr());
-
-//                    chatResult.setContent(e.getMessage());
-
-                    // 由于中止导致的错误信息叠加 (一并保存进数据库)
-                    if (StrUtil.isNotEmpty(allThinkContent.toString())) {
-                        if (isThinking) {
-                            chatResult.setReasoning_content(allThinkContent.toString() + " " + message);
-                        } else {
-                            chatResult.setReasoning_content(allThinkContent.toString());
-                        }
-                    }
-                    chatResult.setContent(allReplyContent.toString() + " " + message);
+                System.out.println("-------------------- 结束流式回答. --------------------");
 
-                    redisUtil.delete(requestOfRedisKey);
+                contentDuration = System.currentTimeMillis() - allStartTime;
 
-                    return chatResult;
+                System.out.println("全部推理: " + allThinkContent);
+                System.out.println("全部回答: " + allReplyContent);
+                System.out.println("总输出耗时: " + contentDuration + " 毫秒");
+                System.out.println("---------------------------------------------------");
 
-//                    return chatResult;
+                if (StrUtil.isNotEmpty(allThinkContent.toString())) {
+                    chatResult.setReasoning_content(allThinkContent.toString());
+                    chatResult.setReasoning_duration(System.currentTimeMillis() - allStartTime);
                 }
-            } catch (Exception e) {
-                System.out.println("Exception(2): " + e.getMessage());
-                // [SSE] 发送消息
-                String contentType = (isThinking ? "THINK_ABORT" : "REPLY_ABORT");
-                ChatSseMessage chatSseMessage = new ChatSseMessage(contentType, e.getMessage(), contentDuration, history_code);
-                sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, chatSseMessage).toJsonStr());
+                chatResult.setContent(allReplyContent.toString());
+                chatResult.setContent_duration(contentDuration);
+                return chatResult;
 
-                redisUtil.delete(requestOfRedisKey);
+        } catch (Exception e) {
+            System.out.println("Exception(2): " + e.getMessage());
+            // [SSE] 发送消息
+            String contentType = (isThinking.get() ? "THINK_ABORT" : "REPLY_ABORT");
+            ChatSseMessage chatSseMessage = new ChatSseMessage(contentType, e.getMessage(), contentDuration, history_code);
+            sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, chatSseMessage).toJsonStr());
 
-                chatResult.setContent(e.getMessage());
-                return chatResult;
-            } finally {
+            redisUtil.delete(requestOfRedisKey);
 
-                // [SSE] 发送消息 (完成)
-                ChatSseMessage chatSseMessage = new ChatSseMessage("REPLY", "[DONE][REPLY]", contentDuration, history_code);
-                sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, chatSseMessage).toJsonStr());
+            chatResult.setContent(e.getMessage());
+            return chatResult;
+        } finally {
 
-            }
+            // [SSE] 发送消息 (完成)
+            ChatSseMessage chatSseMessage = new ChatSseMessage("REPLY", "[DONE][REPLY]", contentDuration, history_code);
+            sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, chatSseMessage).toJsonStr());
+
+        }
 
     }
 }

+ 378 - 0
src/main/java/com/backendsys/modules/sdk/deepseek/utils/OllamaUtil_bak.java

@@ -0,0 +1,378 @@
+//package com.backendsys.modules.sdk.deepseek.utils;
+//
+//import cn.hutool.core.util.ObjectUtil;
+//import cn.hutool.core.util.StrUtil;
+//import com.alibaba.fastjson.JSONObject;
+//import com.backendsys.modules.ai.chat.entity.Chat;
+//import com.backendsys.modules.ai.chat.entity.ChatCompletionParam;
+//import com.backendsys.modules.ai.chat.entity.ChatResult;
+//import com.backendsys.modules.ai.chat.entity.ChatSseMessage;
+//import com.backendsys.modules.common.config.redis.utils.RedisUtil;
+//import com.backendsys.modules.sdk.bocha.entity.BochaParam;
+//import com.backendsys.modules.sdk.bocha.service.BochaService;
+//import com.backendsys.modules.sdk.deepseek.entity.DSRequest;
+//import com.backendsys.modules.sdk.deepseek.entity.DSRequestMessage;
+//import com.backendsys.modules.sse.entity.SseResponse;
+//import com.backendsys.modules.sse.entity.SseResponseEnum;
+//import com.backendsys.modules.sse.utils.SseUtil;
+//import com.fasterxml.jackson.databind.JsonNode;
+//import com.fasterxml.jackson.databind.ObjectMapper;
+//import org.apache.http.client.methods.HttpPost;
+//import org.apache.http.entity.StringEntity;
+//import org.springframework.beans.factory.annotation.Autowired;
+//import org.springframework.beans.factory.annotation.Value;
+//import org.springframework.stereotype.Component;
+//import org.apache.http.client.methods.CloseableHttpResponse;
+//import org.apache.http.impl.client.CloseableHttpClient;
+//import org.apache.http.impl.client.HttpClients;
+//
+//
+//
+//import java.io.BufferedReader;
+//import java.io.InputStreamReader;
+//import java.nio.charset.StandardCharsets;
+//import java.util.ArrayList;
+//import java.util.Collections;
+//import java.util.List;
+//
+///**
+// * ollama run deepseek-r1:1.5b
+// *
+// * Ollama API
+// * https://github.com/ollama/ollama/blob/main/docs/api.md
+// */
+//@Component
+//public class OllamaUtil {
+//
+//    @Autowired
+//    private SseUtil sseUtil;
+//    @Autowired
+//    private RedisUtil redisUtil;
+//    @Autowired
+//    private BochaService bochaService;
+//
+//    @Value("${spring.application.name}")
+//    private String APPLICATION_NAME;
+//    @Value("${deepseek-r1.domain}")
+//    private String DOMAIN;
+//
+//    /**
+//     * 流式对话
+//     */
+//    public ChatResult chatCompletion(ChatCompletionParam chatCompletionParam) {
+//
+//        // 参数化
+//        Long user_id = chatCompletionParam.getUser_id();
+//        String model = chatCompletionParam.getModel();
+//        String prompt = chatCompletionParam.getPrompt();
+//        String history_code = chatCompletionParam.getHistory_code();
+//        List<Chat> chatList = chatCompletionParam.getChatList();
+//        Boolean internet = chatCompletionParam.getInternet();
+//
+//        // 定义作用于全局的变量
+//        Long contentDuration = 0L;
+//        Boolean isThinking = false;
+//        StringBuilder allReplyContent = new StringBuilder();
+//        StringBuilder allThinkContent = new StringBuilder();
+//        String requestOfRedisKey = APPLICATION_NAME + "-chat-history-" + history_code;
+//
+//        ChatResult chatResult = new ChatResult();
+////        try {
+//            System.out.println("向模型: " + model + " 提问: " + prompt);
+//
+//            // 记录请求开始时间
+//            long allStartTime = System.currentTimeMillis();
+//
+//            // 加入上下文历史对话
+//            System.out.println("---- 历史对话 (history_code): " + history_code + " ----");
+//
+//            List<DSRequestMessage> messages = new ArrayList<>();
+//
+//            if (chatList != null && !chatList.isEmpty()) {
+//                chatList.stream().forEach(chat -> {
+//                    if (!"THINK".equals(chat.getContent_type())) {
+//                        messages.add(new DSRequestMessage(chat.getRole(), chat.getContent()));
+//                    }
+//                });
+//                // 反转列表
+//                Collections.reverse(messages);
+//            }
+//
+//            // 【要把搜索到的内容塞到 'user' 里?】
+//
+//            // -- [博查] Web Search API ----------------------------------------------
+//            if (internet) {
+//
+//                // 远程查询、统计接口时间、设置返回参数
+//                long internetStartTime = System.currentTimeMillis();
+//                JsonNode searchResult = bochaService.WebSearch(new BochaParam(prompt));
+//                String context = bochaService.WebSearchToString(searchResult);
+//                context = context.replace("\n", "\\n");
+//
+//                long internetEndTime = System.currentTimeMillis();
+//                chatResult.setInternet_duration(internetStartTime - internetEndTime);
+//                chatResult.setInternet_content(context);
+//
+//                // 将搜索结果作为上下文添加到消息中
+//                messages.add(new DSRequestMessage("system", context));
+//                messages.add(new DSRequestMessage("user", "在回答时引用以上全部数据进行分析")); // 的 "name"、"summary"
+//                messages.add(new DSRequestMessage("assistant", "好的"));
+//
+//                // [SSE] 发送消息
+//                ChatSseMessage chatSearchSseMessage = new ChatSseMessage("SEARCH", context, null, history_code);
+//                sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, chatSearchSseMessage).toJsonStr());
+//            }
+//            // -----------------------------------------------------------------------
+//
+//            // 新的对话内容
+//            messages.add(new DSRequestMessage("user", prompt));
+//
+//            // 输出全部对话内容
+//            messages.stream().forEach(msg -> {
+//                System.out.println("[" + msg.getRole() + "]: " + msg.getContent());
+//            });
+//            System.out.println("---------------------------------------------------------------------");
+//
+//            ObjectMapper objectMapper = new ObjectMapper();
+//            try (CloseableHttpClient client = HttpClients.createDefault()) {
+//
+//                /*
+//                【/api/generate】
+//                它是一个相对基础的文本生成端点,主要用于根据给定的提示信息生成一段连续的文本。
+//                这个端点会基于输入的提示,按照模型的语言生成能力输出一段完整的内容,更侧重于单纯的文本生成任务。
+//                生成过程不依赖于上下文的历史对话信息,每次请求都是独立的,模型仅依据当前输入的提示进行文本生成。
+//                {
+//                    "model": "llama2", "prompt": "请描述一下美丽的海滩", "num_predict": 200, "temperature": 0.7
+//                }
+//
+//                【/api/chat】
+//                该端点专为模拟聊天场景设计,具备处理对话上下文的能力。它可以跟踪对话的历史记录,理解对话的上下文信息,从而生成更符合对话逻辑和连贯性的回复。
+//                更注重模拟真实的人机对话交互,能够根据历史对话和当前输入生成合适的回应,适用于构建聊天机器人等交互式应用。
+//                {
+//                    "model": "deepseek-r1:1.5b",
+//                    "messages": [
+//                        {
+//                            "role": "system",
+//                            "content": "你是一个能够理解中文指令并帮助完成任务的智能助手。你的任务是根据用户的需求生成合适的分类任务或生成任务,并准确判断这些任务的类型。请确保你的回答简洁、准确且符合中英文语境。"
+//                        },
+//                        {
+//                            // "role": "assistant",
+//                            "role": "user",
+//                            "content": "写一个简单的 Python 函数,用于计算两个数的和"
+//                        }
+//                    ],
+//                    "stream": false,
+//
+//                    // 新增
+//                    "context": ["引用1","引用2"]
+//                }
+//                 */
+//
+//
+//                // [Chat] 构建请求体
+//                HttpPost request = new HttpPost(DOMAIN + "/api/chat");
+//                DSRequest body = new DSRequest();
+//                body.setModel(model);
+//                body.setMessages(messages);
+//                body.setStream(true);
+//                String requestBody = objectMapper.writeValueAsString(body);
+//
+////                // [Generate] 构建请求体
+////                HttpPost request = new HttpPost(DOMAIN + "/api/generate");
+////                Map<String, Object> requestMap = new HashMap<>();
+////                requestMap.put("model", model);
+////                requestMap.put("prompt", prompt);
+////                requestMap.put("stream", true);
+////                String requestBody = objectMapper.writeValueAsString(requestMap);
+//
+//                request.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8));
+//
+//                try (CloseableHttpResponse response = client.execute(request);
+//                     BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8))) {
+//
+//                    long apiDuration = System.currentTimeMillis() - allStartTime;   // 接口耗时
+//
+//                    long thinkStartTime = 0L;                            // 开始思考时间
+//                    long thinkDuration = 0L;                             // 思考耗时
+//
+//                    System.out.println("API 调用耗时: " + apiDuration + " 毫秒");
+//                    System.out.println("---- 开始流式回答: ------------------------------------");
+//
+//                    // [SSE] 发送消息
+//                    ChatSseMessage chatLoadingSseMessage = new ChatSseMessage("LOADING", "正在思考", null, history_code);
+//                    sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, chatLoadingSseMessage).toJsonStr());
+//
+//                    String line;
+//                    while ((line = reader.readLine()) != null) {
+//
+//                        // 判断是否中止
+//                        if (ObjectUtil.isEmpty(redisUtil.getCacheObject(requestOfRedisKey))) {
+//                            System.out.println("中止!");
+//                            request.abort();
+//                            // 流程结束后,删除锁
+//                            redisUtil.delete(requestOfRedisKey);
+//                            break;
+//                        }
+//
+//
+//                        // System.out.println(line);
+//                        /*
+//                            ---------------------- [Chat] line ----------------------
+//                            {"model":"deepseek-r1:1.5b","created_at":"2025-03-18T07:37:06.483163789Z","message":{"role":"assistant","content":"\u003cthink\u003e"},"done":false}
+//
+//                            ---------------------- [Generate] line ------------------
+//                            {"model":"deepseek-r1:1.5b","created_at":"2025-03-05T10:51:17.443189986Z","response":"\u003cthink\u003e","done":false}
+//                            {"model":"deepseek-r1:1.5b","created_at":"2025-03-06T11:08:30.9219611Z","response":"\n\n","done":false}
+//
+//                            ---------------------- [Error] line ---------------------
+//                            {"error":"llama runner process has terminated: error loading model: unable to allocate CUDA0 buffer"}
+//                         */
+//
+//                        // 每行数据可以是一个JSON对象,根据实际情况处理
+//                        JSONObject resJson = JSONObject.parseObject(line);
+//
+//                        // -- 判断内容是否为空 (或报错) --------------------------------------
+//                        if (resJson == null) return null;
+//                        String errJsonMessage = resJson.getString("error");
+//                        if (errJsonMessage != null) {
+//                            // [SSE] 发送消息
+//                            sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, new ChatSseMessage("REPLY", errJsonMessage, contentDuration, history_code)).toJsonStr());
+//                            // [SSE] 发送消息 (完成)
+//                            sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, new ChatSseMessage("REPLY", "[DONE][REPLY]", contentDuration, history_code)).toJsonStr());
+//                            //
+//                            chatResult.setContent(errJsonMessage);
+//                            return chatResult;
+//                        }
+//
+//                        // --------------------------------------------------------------
+//                        // [Chat]
+//                        JSONObject resJsonMessage = resJson.getJSONObject("message");
+//                        String content = resJsonMessage.getString("content");
+//
+////                        // [Generate]
+////                        String content = resJson.getString("response");
+//                        // --------------------------------------------------------------
+//
+//                        // 开始思考
+//                        if (content.contains("<think>")) {
+//                            isThinking = true;
+//                            thinkStartTime = System.currentTimeMillis();
+//                        }
+//                        // 停止思考,并计算思考耗时
+//                        if (content.contains("</think>")) {
+//                            isThinking = false;
+//                            thinkDuration = thinkStartTime - allStartTime;
+//                            System.out.println("推理耗时: " + thinkDuration + "毫秒");
+//                            System.out.println("-----------------------------------------------------");
+//
+//                            if (allThinkContent.length() > 0){
+//                                // [SSE] 发送消息
+//                                ChatSseMessage chatSseMessage = new ChatSseMessage("THINK", "[DONE][THINK]", thinkDuration, history_code);
+//                                sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, chatSseMessage).toJsonStr());
+//                            }
+//
+//                        }
+//
+//                        // [思考] Think
+//                        if (isThinking) {
+//                            if (!content.contains("<think>") && !content.contains("\n\n") && !content.contains("\n")) {
+//                                // [SSE] 发送消息
+//                                ChatSseMessage chatSseMessage = new ChatSseMessage("THINK", content, null, history_code);
+//                                sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, chatSseMessage).toJsonStr());
+//                                // 收集推理内容
+//                                allThinkContent.append(content);
+//                            }
+//                        }
+//
+//                        // [回答] Reply
+//                        if (!isThinking) {
+//
+//                            // System.out.println("content: " + content);
+//
+//                            if (!content.contains("</think>") && !content.contains("\n\n")) {
+//
+//                                Boolean done = resJson.getBoolean("done");
+//                                if (!done) {
+//
+//                                    // [SSE] 发送消息
+//                                    ChatSseMessage chatSseMessage = new ChatSseMessage("REPLY", content, null, history_code);
+//                                    sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, chatSseMessage).toJsonStr());
+//                                    // 收集回答内容
+//                                    allReplyContent.append(content);
+//
+//                                }
+//                            }
+//                        }
+//
+//                    }
+//
+//
+//                    System.out.println("-------------------- 结束流式回答. --------------------");
+//                    contentDuration = System.currentTimeMillis() - allStartTime;
+//
+//                    System.out.println("全部推理: " + allThinkContent);
+//                    System.out.println("全部回答: " + allReplyContent);
+//                    System.out.println("总输出耗时: " + contentDuration + " 毫秒");
+//                    System.out.println("---------------------------------------------------");
+//
+//                    if (StrUtil.isNotEmpty(allThinkContent.toString())) {
+//                        chatResult.setReasoning_content(allThinkContent.toString());
+//                        chatResult.setReasoning_duration(thinkDuration);
+//                    }
+//                    chatResult.setContent(allReplyContent.toString());
+//                    chatResult.setContent_duration(contentDuration);
+//                    return chatResult;
+//
+//                } catch (Exception e) {
+//                    System.out.println("Exception(1): " + e.getMessage());
+//                    String message = e.getMessage();
+//                    if (message.contains("failed to respond")) {
+//                        message = "(系统繁忙,请稍后再试)";
+//                    }
+//                    if (message.contains("Premature end of chunk coded message body: closing chunk expected")) {
+//                        message = "(请求中止)";
+//                    }
+//                    // [SSE] 发送消息
+//                    String contentType = (isThinking ? "THINK_ABORT" : "REPLY_ABORT");
+//                    ChatSseMessage chatSseMessage = new ChatSseMessage(contentType, message, contentDuration, history_code);
+//                    sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, chatSseMessage).toJsonStr());
+//
+////                    chatResult.setContent(e.getMessage());
+//
+//                    // 由于中止导致的错误信息叠加 (一并保存进数据库)
+//                    if (StrUtil.isNotEmpty(allThinkContent.toString())) {
+//                        if (isThinking) {
+//                            chatResult.setReasoning_content(allThinkContent.toString() + " " + message);
+//                        } else {
+//                            chatResult.setReasoning_content(allThinkContent.toString());
+//                        }
+//                    }
+//                    chatResult.setContent(allReplyContent.toString() + " " + message);
+//
+//                    redisUtil.delete(requestOfRedisKey);
+//
+//                    return chatResult;
+//
+////                    return chatResult;
+//                }
+//            } catch (Exception e) {
+//                System.out.println("Exception(2): " + e.getMessage());
+//                // [SSE] 发送消息
+//                String contentType = (isThinking ? "THINK_ABORT" : "REPLY_ABORT");
+//                ChatSseMessage chatSseMessage = new ChatSseMessage(contentType, e.getMessage(), contentDuration, history_code);
+//                sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, chatSseMessage).toJsonStr());
+//
+//                redisUtil.delete(requestOfRedisKey);
+//
+//                chatResult.setContent(e.getMessage());
+//                return chatResult;
+//            } finally {
+//
+//                // [SSE] 发送消息 (完成)
+//                ChatSseMessage chatSseMessage = new ChatSseMessage("REPLY", "[DONE][REPLY]", contentDuration, history_code);
+//                sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, chatSseMessage).toJsonStr());
+//
+//            }
+//
+//    }
+//}

+ 0 - 4
src/main/java/com/backendsys/modules/sdk/tencentcloud/huanyuan/service/impl/HunYuanClientImpl.java

@@ -7,10 +7,8 @@ import com.backendsys.modules.ai.chat.entity.ChatCompletionParam;
 import com.backendsys.modules.ai.chat.entity.ChatResult;
 import com.backendsys.modules.ai.chat.entity.ChatSseMessage;
 import com.backendsys.modules.common.config.redis.utils.RedisUtil;
-import com.backendsys.modules.common.config.security.utils.SecurityUtil;
 import com.backendsys.modules.sdk.bocha.entity.BochaParam;
 import com.backendsys.modules.sdk.bocha.service.BochaService;
-import com.backendsys.modules.sdk.deepseek.entity.DSRequestMessage;
 import com.backendsys.modules.sdk.tencentcloud.huanyuan.service.HunYuanClient;
 import com.backendsys.modules.sse.entity.SseResponse;
 import com.backendsys.modules.sse.entity.SseResponseEnum;
@@ -29,8 +27,6 @@ import com.tencentcloudapi.hunyuan.v20230901.models.ChatStdResponse;
 import com.tencentcloudapi.hunyuan.v20230901.models.Message;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.stereotype.Service;
 
 import java.util.ArrayList;

+ 297 - 0
src/main/java/com/backendsys/modules/sdk/tencentcloud/huanyuan/service/impl/HunYuanClientImpl222.java

@@ -0,0 +1,297 @@
+//package com.backendsys.modules.sdk.tencentcloud.huanyuan.service.impl;
+//
+//import cn.hutool.core.util.ArrayUtil;
+//import cn.hutool.core.util.ObjectUtil;
+//import com.backendsys.modules.ai.chat.entity.Chat;
+//import com.backendsys.modules.ai.chat.entity.ChatCompletionParam;
+//import com.backendsys.modules.ai.chat.entity.ChatResult;
+//import com.backendsys.modules.ai.chat.entity.ChatSseMessage;
+//import com.backendsys.modules.common.config.redis.utils.RedisUtil;
+//import com.backendsys.modules.sdk.bocha.entity.BochaParam;
+//import com.backendsys.modules.sdk.bocha.service.BochaService;
+//import com.backendsys.modules.sdk.tencentcloud.huanyuan.service.HunYuanClient;
+//import com.backendsys.modules.sse.entity.SseResponse;
+//import com.backendsys.modules.sse.entity.SseResponseEnum;
+//import com.backendsys.modules.sse.utils.SseUtil;
+//import com.fasterxml.jackson.core.JsonProcessingException;
+//import com.fasterxml.jackson.databind.JsonMappingException;
+//import com.fasterxml.jackson.databind.JsonNode;
+//import com.fasterxml.jackson.databind.ObjectMapper;
+//import com.google.gson.Gson;
+//import com.google.gson.GsonBuilder;
+//import com.tencentcloudapi.common.Credential;
+//import com.tencentcloudapi.common.SSEResponseModel;
+//import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+//import com.tencentcloudapi.common.profile.ClientProfile;
+//import com.tencentcloudapi.hunyuan.v20230901.HunyuanClient;
+//import com.tencentcloudapi.hunyuan.v20230901.models.ChatCompletionsRequest;
+//import com.tencentcloudapi.hunyuan.v20230901.models.ChatCompletionsResponse;
+//import com.tencentcloudapi.hunyuan.v20230901.models.Choice;
+//import com.tencentcloudapi.hunyuan.v20230901.models.Message;
+//import org.springframework.beans.factory.annotation.Autowired;
+//import org.springframework.beans.factory.annotation.Value;
+//import org.springframework.stereotype.Service;
+//
+//import java.util.ArrayList;
+//import java.util.Collections;
+//import java.util.List;
+//
+//@Service
+//public class HunYuanClientImpl implements HunYuanClient {
+//
+//    @Value("${tencent.hunyuan.secret-id}")
+//    private String SECRET_ID;
+//    @Value("${tencent.hunyuan.secret-key}")
+//    private String SECRET_KEY;
+//    @Value("${tencent.hunyuan.region}")
+//    private String REGION;
+//
+//    @Value("${spring.application.name}")
+//    private String APPLICATION_NAME;
+//
+//    @Autowired
+//    private SseUtil sseUtil;
+//    @Autowired
+//    private RedisUtil redisUtil;
+//    @Autowired
+//    private BochaService bochaService;
+//
+//    private Message setMessage(String role, String content) {
+//        Message msg = new Message();
+//        msg.setRole(role);
+//        msg.setContent(content);
+//        return msg;
+//    }
+//
+//    /**
+//     * [HunYuan] 发起对话
+//     * https://cloud.tencent.com/document/product/1729/101836
+//     */
+//    @Override
+//    public ChatResult chatCompletion(ChatCompletionParam chatCompletionParam) {
+//
+//        // 参数化
+//        String prompt = chatCompletionParam.getPrompt();
+//        System.out.println("向混元模型 提问: " + prompt);
+//
+//        Long user_id = chatCompletionParam.getUser_id();
+//        String history_code = chatCompletionParam.getHistory_code();
+//        List<Chat> chatList = chatCompletionParam.getChatList();
+//        Boolean internet = chatCompletionParam.getInternet();
+//
+//        // 定义作用于全局的变量
+//        Long replyDuration = 0L;
+//        String requestOfRedisKey = APPLICATION_NAME + "-chat-history-" + history_code;
+//        StringBuilder allReplyContent = new StringBuilder();
+//
+//        // 记录请求开始时间
+//        long allStartTime = System.currentTimeMillis();
+//
+//        // 加入上下文历史对话
+//        System.out.println("---- 历史对话 (history_code): " + history_code + " ----");
+//
+//        List<Message> messages = new ArrayList<>();
+//        if (chatList != null && !chatList.isEmpty()) {
+//            chatList.stream().forEach(chat -> {
+//                // 混元没有 THINK
+//                messages.add(setMessage(chat.getRole(), chat.getContent()));
+//            });
+//            // 反转列表
+//            Collections.reverse(messages);
+//        }
+//
+//        // 返回值结构体
+//        ChatResult chatResult = new ChatResult();
+//
+//        // -- [博查] Web Search API ----------------------------------------------
+//        if (internet) {
+//
+//            // 远程查询、统计接口时间、设置返回参数
+//            long internetStartTime = System.currentTimeMillis();
+//            JsonNode searchResult = bochaService.WebSearch(new BochaParam(prompt));
+//            String context = bochaService.WebSearchToString(searchResult);
+//            long internetEndTime = System.currentTimeMillis();
+//            chatResult.setInternet_duration(internetStartTime - internetEndTime);
+//            chatResult.setInternet_content(context);
+//
+//            // 将搜索结果作为上下文添加到消息中
+//            messages.add(setMessage("system", context));
+//            messages.add(setMessage("user", "在回答时引用以上全部数据进行分析")); // 的 "name"、"summary"
+//            messages.add(setMessage("assistant", "好的"));
+//
+//            // [SSE] 发送消息
+//            ChatSseMessage chatSearchSseMessage = new ChatSseMessage("SEARCH", context, null, history_code);
+//            sseUtil.send(user_id, new SseResponse(SseResponseEnum.OLLAMA, chatSearchSseMessage).toJsonStr());
+//        }
+//        // -----------------------------------------------------------------------
+//
+//        // 新的对话内容
+//        messages.add(setMessage("user", prompt));
+//
+//        // 输出全部对话内容
+//        messages.stream().forEach(msg -> {
+//            System.out.println("[" + msg.getRole() + "]: " + msg.getContent());
+//        });
+//        System.out.println("---------------------------------------------------------------------");
+//
+//
+//        // [混元大模型]
+//        Credential cred = new Credential(SECRET_ID, SECRET_KEY);
+//        ClientProfile clientProfile = new ClientProfile();
+//        HunyuanClient client = new HunyuanClient(cred, REGION, clientProfile);
+//
+//        ChatCompletionsRequest req = new ChatCompletionsRequest();
+//        // 模型名称,可选值包括 hunyuan-lite、hunyuan-standard、hunyuan-standard-256K、hunyuan-pro、
+//        //      hunyuan-code、 hunyuan-role、 hunyuan-functioncall、 hunyuan-vision、 hunyuan-turbo。
+//        // 各模型介绍请阅读 [产品概述](https://cloud.tencent.com/document/product/1729/104753) 中的说明。
+//        // 注意:不同的模型计费不同,请根据 [购买指南](https://cloud.tencent.com/document/product/1729/97731) 按需调用。
+//        req.setModel("hunyuan-standard");
+//        req.setMessages(ArrayUtil.toArray(messages, Message.class));
+//
+//        try {
+//
+//            try (ChatCompletionsResponse resp = client.ChatCompletions(req)) {
+//                Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
+//                for (SSEResponseModel.SSE e : resp) {
+//                    ChatCompletionsResponse eventModel = gson.fromJson(e.Data, ChatCompletionsResponse.class);
+//                    Choice[] choices = eventModel.getChoices();
+//                    if (choices.length > 0) {
+//                        System.out.println(choices[0].getDelta().getContent());
+//                    }
+//                }
+//            }
+//
+////            // 发送对话
+////            try (ChatCompletionsResponse resp = client.ChatCompletions(req)) {
+////
+////                // hunyuan ChatCompletions 同时支持 stream 和非 stream 的情况
+////                req.setStream(true);
+////                if (req.getStream()) {
+////
+////                    // stream 示例
+////                    Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
+////                    for (SSEResponseModel.SSE e : resp) {
+////
+////                        System.out.println("e = " + e);
+////                        System.out.println("data = " + e.Data);
+////
+////                        ChatCompletionsResponse eventModel = gson.fromJson(e.Data, ChatCompletionsResponse.class);
+////                        Choice[] choices = eventModel.getChoices();
+////
+////                        System.out.println("choices = " + choices);
+////
+////                        if (choices.length > 0) {
+////
+////                            String content = choices[0].getDelta().getContent();
+////                            System.out.println("content = " + content);
+////
+////                            // [SSE] 发送消息
+////                            ChatSseMessage chatSseMessage = new ChatSseMessage("REPLY", content, null, history_code);
+////                            sseUtil.send(user_id, new SseResponse(SseResponseEnum.HUNYUAN, chatSseMessage).toJsonStr());
+////
+////                            // 收集回答内容
+////                            allReplyContent.append(content);
+////
+////                            // 判断是否中止
+////                            if (ObjectUtil.isEmpty(redisUtil.getCacheObject(requestOfRedisKey))) {
+////                                System.out.println("中止!");
+////                                allReplyContent.append(" (请求中止)");
+////                                //                    request.abort();
+////                                // 流程结束后,删除锁
+////                                redisUtil.delete(requestOfRedisKey);
+////                                break;
+////                            }
+////
+////                        }
+////
+//////                        // 如果希望在任意时刻中止事件流, 使用 break 即可
+//////                        boolean iWantToCancelNow = false;
+//////                        if (iWantToCancelNow) {
+//////                            break;
+//////                        }
+////                    }
+////
+////
+//////                    for (SSEResponseModel.SSE e : resp) {
+//////
+//////                        //                 System.out.println(e.Data);
+//////                        /**
+//////                         *
+//////                         * .Data:
+//////                         *  {"Note":"以上内容为AI生成,不代表开发者立场,请勿删除或修改本标记","Choices":[{"FinishReason":"","Delta":{"Role":"assistant","Content":"当然"}}],"Created":1709618061,"Id":"e73c0a71-5c98-4893-ba90-ad5056d5871a","Usage":{"PromptTokens":7,"CompletionTokens":1,"TotalTokens":8}}
+//////                         * e.Data:
+//////                         *  {"Note":"以上内容为AI生成,不代表开发者立场,请勿删除或修改本标记","Choices":[{"FinishReason":"","Delta":{"Role":"assistant","Content":"可以"}}],"Created":1709618061,"Id":"e73c0a71-5c98-4893-ba90-ad5056d5871a","Usage":{"PromptTokens":7,"CompletionTokens":2,"TotalTokens":9}}
+//////                         * e.Data:
+//////                         *  {"Note":"以上内容为AI生成,不代表开发者立场,请勿删除或修改本标记","Choices":[{"FinishReason":"","Delta":{"Role":"assistant","Content":","}}],"Created":1709618061,"Id":"e73c0a71-5c98-4893-ba90-ad5056d5871a","Usage":{"PromptTokens":7,"CompletionTokens":3,"TotalTokens":10}}
+//////                         * e.Data:
+//////                         */
+//////
+//////                        ObjectMapper objectMapper = new ObjectMapper();
+//////                        JsonNode node = objectMapper.readTree(e.Data.toString());
+//////                        JsonNode delta = node.path("Choices").path(0).path("Delta");
+//////                        String content = delta.path("Content").asText("");
+//////
+//////                        // [SSE] 发送消息
+//////                        ChatSseMessage chatSseMessage = new ChatSseMessage("REPLY", content, null, history_code);
+//////                        sseUtil.send(user_id, new SseResponse(SseResponseEnum.HUNYUAN, chatSseMessage).toJsonStr());
+//////
+//////                        // 收集回答内容
+//////                        allReplyContent.append(content);
+//////
+//////                        // 判断是否中止
+//////                        if (ObjectUtil.isEmpty(redisUtil.getCacheObject(requestOfRedisKey))) {
+//////                            System.out.println("中止!");
+//////                            allReplyContent.append(" (请求中止)");
+//////                            //                    request.abort();
+//////                            // 流程结束后,删除锁
+//////                            redisUtil.delete(requestOfRedisKey);
+//////                            break;
+//////                        }
+//////
+//////                    }
+//////
+////                    System.out.println("-------------------- 结束流式回答. --------------------");
+////                    replyDuration = System.currentTimeMillis() - allStartTime;
+////
+////                    System.out.println("全部回答: " + allReplyContent);
+////                    System.out.println("总输出耗时: " + replyDuration + " 毫秒");
+////
+////                    chatResult.setContent(allReplyContent.toString());
+////                    chatResult.setContent_duration(replyDuration);
+////                    return chatResult;
+////
+////                } else {
+////                    // 非 stream 示例
+////                    // 通过 Stream=false 参数来指定非 stream 协议, 一次性拿到结果
+////                    String content = client.ChatCompletions(req).getChoices()[0].getMessage().getContent();
+////                    System.out.println("content = " + content);
+////                    chatResult.setContent(content);
+////                    return chatResult;
+////                }
+//////
+//////            } catch (JsonMappingException e) {
+//////                throw new RuntimeException(e);
+//////            } catch (JsonProcessingException e) {
+//////                throw new RuntimeException(e);
+////            }
+//
+//        } catch (TencentCloudSDKException e) {
+//            System.out.println("TencentCloudSDKException: " + e.getMessage());
+//            // [SSE] 发送消息
+//            ChatSseMessage chatSseMessage = new ChatSseMessage("REPLY", e.getMessage(), replyDuration, history_code);
+//            sseUtil.send(user_id, new SseResponse(SseResponseEnum.HUNYUAN, chatSseMessage).toJsonStr());
+//
+//            chatResult.setContent(e.getMessage());
+//            return chatResult;
+//        } finally {
+//            // [SSE] 发送消息
+//            ChatSseMessage chatSseMessage = new ChatSseMessage("REPLY", "[DONE][REPLY]", replyDuration, history_code);
+//            sseUtil.send(user_id, new SseResponse(SseResponseEnum.HUNYUAN, chatSseMessage).toJsonStr());
+//
+//            redisUtil.delete(requestOfRedisKey);
+//        }
+//
+//        return null;
+//
+//    }
+//}

+ 1 - 0
src/main/java/com/backendsys/modules/sse/entity/SseResponseEnum.java

@@ -7,6 +7,7 @@ public enum SseResponseEnum {
     LOGOUT("logout", "退出登录"),
     NOTICE("notice", "通知"),
     UPLOAD("upload", "上传"),
+    COMFYUI("comfyui", "ComfyUI"),
     OLLAMA("ollama", "Ollama"),
     DEEPSEEK("deepseek", "Deepseek"),
     HUNYUAN("hunyuan", "Hunyuan");

+ 5 - 1
src/main/resources/application-dev.yml

@@ -188,4 +188,8 @@ klingai:
   url: https://api-beijing.klingai.com
   access-key: A9baBTPFLH8RfrAHGeb4mGagmRHhRHTg
   secret-key: PQnT4E9TMYkPN93pb8JCHJC3dtFAtNPC
-  token-duration-time: 10000
+  token-duration-time: 10000
+
+comfyui:
+  host: 127.0.0.1
+  token: $2b$12$.MR4qGaFetN1FPQzbfyIrehsyjnPJ12xAZhR/l7KZpLkUPQTCG4gy

+ 5 - 1
src/main/resources/application-local.yml

@@ -201,4 +201,8 @@ klingai:
   url: https://api-beijing.klingai.com
   access-key: A9baBTPFLH8RfrAHGeb4mGagmRHhRHTg
   secret-key: PQnT4E9TMYkPN93pb8JCHJC3dtFAtNPC
-  token-duration-time: 10000
+  token-duration-time: 10000
+
+comfyui:
+  host: 43.128.1.201
+  token: $2b$12$.MR4qGaFetN1FPQzbfyIrehsyjnPJ12xAZhR/l7KZpLkUPQTCG4gy

+ 5 - 1
src/main/resources/application-prod.yml

@@ -189,4 +189,8 @@ klingai:
   url: https://api-beijing.klingai.com
   access-key: A9baBTPFLH8RfrAHGeb4mGagmRHhRHTg
   secret-key: PQnT4E9TMYkPN93pb8JCHJC3dtFAtNPC
-  token-duration-time: 10000
+  token-duration-time: 10000
+
+comfyui:
+  host: 127.0.0.1
+  token: $2b$12$.MR4qGaFetN1FPQzbfyIrehsyjnPJ12xAZhR/l7KZpLkUPQTCG4gy