From 90241d7a6cc75168849aab50fefce3f8187916fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=BE=E4=B9=BE?= <1046762075@qq.com> Date: Fri, 5 Dec 2025 13:36:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9Eminio=E5=AD=98?= =?UTF-8?q?=E5=82=A8=E3=80=81=E5=AF=B9=E6=8E=A5=E5=90=84=E4=B8=AA=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E6=B7=B1=E5=BA=A6=E6=80=9D=E8=80=83=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/install/docker/docker-compose.yml | 25 ++ docs/install/sql/luohuo_dev.sql | 38 +- docs/install/sql/luohuo_im_01.sql | 52 ++- .../model/deepseek/DeepSeekSseClient.java | 114 ++++++ .../core/model/openai/OpenAiCompatClient.java | 80 ++++ .../model/openai/OpenAiCompatSseClient.java | 130 ++++++ .../model/strategy/chat/ChatCallStrategy.java | 13 + .../strategy/chat/ChatStrategyFactory.java | 44 +++ .../strategy/chat/ChatStreamingStrategy.java | 10 + .../chat/DeepSeekStreamingStrategy.java | 20 + .../chat/OpenAiCompatCallStrategy.java | 18 + .../chat/OpenAiCompatStreamingStrategy.java | 20 + .../model/strategy/chat/ReasoningChunk.java | 6 + .../chat/AiChatMessageServiceImpl.java | 371 +++++++++++++----- .../oauth/granter/AbstractTokenGranter.java | 1 + .../oauth/granter/RefreshTokenGranter.java | 17 +- .../com/luohuo/flex/entity/IceServer.java | 13 + .../java/com/luohuo/flex/entity/Init.java | 2 + .../luohuo/flex/service/SysConfigService.java | 2 - .../service/impl/SysSysConfigServiceImpl.java | 119 ++---- .../flex/flex/storage/StorageDriver.java | 85 ++-- .../flex/storage/engine/MinioStorage.java | 60 +++ .../luohuo/flex/service/TranslateService.java | 10 + .../service/impl/TranslateServiceImpl.java | 229 +++++++++++ .../java/com/luohuo/flex/IndexController.java | 17 +- .../com/luohuo/flex/TranslateController.java | 85 ++++ .../com/luohuo/flex/dto/TranslateRequest.java | 11 + .../luohuo/flex/dto/TranslateResponse.java | 11 + .../flex/dto/TranslateSegmentsResponse.java | 13 + luohuo-cloud/pom.xml | 2 +- luohuo-util/pom.xml | 4 +- 31 files changed, 1350 insertions(+), 272 deletions(-) create mode 100644 luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/deepseek/DeepSeekSseClient.java create mode 100644 luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/openai/OpenAiCompatClient.java create mode 100644 luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/openai/OpenAiCompatSseClient.java create mode 100644 luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/ChatCallStrategy.java create mode 100644 luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/ChatStrategyFactory.java create mode 100644 luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/ChatStreamingStrategy.java create mode 100644 luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/DeepSeekStreamingStrategy.java create mode 100644 luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/OpenAiCompatCallStrategy.java create mode 100644 luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/OpenAiCompatStreamingStrategy.java create mode 100644 luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/ReasoningChunk.java create mode 100644 luohuo-cloud/luohuo-public/luohuo-config-sdk/src/main/java/com/luohuo/flex/entity/IceServer.java create mode 100644 luohuo-cloud/luohuo-system/luohuo-system-biz/src/main/java/com/luohuo/flex/flex/storage/engine/MinioStorage.java create mode 100644 luohuo-cloud/luohuo-system/luohuo-system-biz/src/main/java/com/luohuo/flex/service/TranslateService.java create mode 100644 luohuo-cloud/luohuo-system/luohuo-system-biz/src/main/java/com/luohuo/flex/service/impl/TranslateServiceImpl.java create mode 100644 luohuo-cloud/luohuo-system/luohuo-system-controller/src/main/java/com/luohuo/flex/TranslateController.java create mode 100644 luohuo-cloud/luohuo-system/luohuo-system-controller/src/main/java/com/luohuo/flex/dto/TranslateRequest.java create mode 100644 luohuo-cloud/luohuo-system/luohuo-system-controller/src/main/java/com/luohuo/flex/dto/TranslateResponse.java create mode 100644 luohuo-cloud/luohuo-system/luohuo-system-controller/src/main/java/com/luohuo/flex/dto/TranslateSegmentsResponse.java diff --git a/docs/install/docker/docker-compose.yml b/docs/install/docker/docker-compose.yml index fec65df4..021520cc 100644 --- a/docs/install/docker/docker-compose.yml +++ b/docs/install/docker/docker-compose.yml @@ -83,6 +83,31 @@ services: environment: - NAMESRV_ADDR=rocketmq-namesrv:9876 command: sh mqproxy + minio: + image: minio/minio:latest + container_name: minio + restart: always + ports: + - "9000:9000" + - "9001:9001" + environment: + - MINIO_ROOT_USER=luohuo + - MINIO_ROOT_PASSWORD=mini.123huo + volumes: + - ./minio/data:/data + - ./minio/config:/root/.minio + command: server --console-address ":9001" /data + minio-mc: + image: minio/mc:latest + container_name: minio-mc + depends_on: + - minio + entrypoint: [ "/bin/sh","-c" ] + command: > + "mc alias set local http://minio:9000 luohuo mini.123huo && + mc mb -p local/dev || true && + mc anonymous set download local/dev && + mc alias ls" jenkins: image: jenkins/jenkins:lts-jdk21 diff --git a/docs/install/sql/luohuo_dev.sql b/docs/install/sql/luohuo_dev.sql index 4a96f8c2..1e3ad83b 100644 --- a/docs/install/sql/luohuo_dev.sql +++ b/docs/install/sql/luohuo_dev.sql @@ -11,7 +11,7 @@ Target Server Version : 80030 (8.0.30) File Encoding : 65001 - Date: 03/12/2025 15:16:20 + Date: 05/12/2025 13:33:00 */ SET NAMES utf8mb4; @@ -106,7 +106,7 @@ CREATE TABLE `ai_chat_conversation` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NULL DEFAULT NULL COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 102357606609921 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 聊天对话表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 103094306752513 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 聊天对话表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of ai_chat_conversation @@ -138,7 +138,7 @@ CREATE TABLE `ai_chat_message` ( `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_msg_type`(`msg_type` ASC) USING BTREE COMMENT '消息内容类型索引' -) ENGINE = InnoDB AUTO_INCREMENT = 102357791159299 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 聊天消息表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 103085163169795 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 聊天消息表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of ai_chat_message @@ -169,7 +169,7 @@ CREATE TABLE `ai_chat_role` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 98376230626305 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 聊天角色表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 102454855742465 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 聊天角色表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of ai_chat_role @@ -372,7 +372,7 @@ CREATE TABLE `ai_model_usage_record` ( `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uk_user_model`(`user_id` ASC, `model_id` ASC) USING BTREE COMMENT '用户-模型唯一索引' -) ENGINE = InnoDB AUTO_INCREMENT = 102357791159297 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 公开模型使用记录表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 103085163169793 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 公开模型使用记录表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of ai_model_usage_record @@ -628,7 +628,7 @@ CREATE TABLE `base_config` ( `tenant_id` bigint NOT NULL, PRIMARY KEY (`id`) USING BTREE, INDEX `key`(`config_key` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 19 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '参数配置表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 115 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '参数配置表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of base_config @@ -636,8 +636,8 @@ CREATE TABLE `base_config` ( INSERT INTO `base_config` VALUES (1, 'system', '{\"title\":\"系统名称\",\"componentType\":\"text\",\"value\":\"Hula-IM\",\"configKey\":\"systemName\",\"type\":\"system\"}', 'systemName', 'HuLa', 0, '2025-06-26 07:42:06.404', '2025-06-26 07:42:06.494', 3, NULL, 0); INSERT INTO `base_config` VALUES (2, 'system', '{\"title\":\"系统Logo\",\"componentType\":\"text\",\"value\":\"/static/img/Iogo.png\",\"configKey\":\"logo\",\"type\":\"system\"}', 'logo', '/static/img/Iogo.png', 0, '2025-06-26 07:42:06.404', '2025-06-26 07:42:06.494', 3, NULL, 0); INSERT INTO `base_config` VALUES (3, 'qiniu_up_config', '{\"title\":\"空间域名 Domain\",\"componentType\":\"text\",\"value\":\"https://upload-z2.qiniup.com\",\"configKey\":\"qnUploadUrl\",\"type\":\"qiniu_up_config\"}', 'qnUploadUrl', 'https://up-z2.qiniup.com', 0, '2025-06-26 07:42:06.404', '2025-08-18 04:29:56.616', 3, NULL, 0); -INSERT INTO `base_config` VALUES (4, 'qiniu_up_config', '{\"title\":\"accessKey\",\"componentType\":\"text\",\"value\":\"231YmL0vGAYFTW-rHl4LgA5_\",\"configKey\":\"qnAccessKey\",\"type\":\"qiniu_up_config\"}', 'qnAccessKey', 'LXrRo6YhT22hy5yScLzQJAQaUGUJ', 0, '2025-06-26 07:42:06.404', '2025-10-11 03:43:10.679', 3, NULL, 0); -INSERT INTO `base_config` VALUES (5, 'qiniu_up_config', '{\"title\":\"SecretKey\",\"componentType\":\"text\",\"value\":\"2daTXFDEG9PuAy4TnvfR1o2\",\"configKey\":\"qnSecretKey\",\"type\":\"qiniu_up_config\"}', 'qnSecretKey', 'BYKKz6n222111VS-llemF2Hg', 0, '2025-06-26 07:42:06.404', '2025-10-11 03:43:05.090', 3, NULL, 0); +INSERT INTO `base_config` VALUES (4, 'qiniu_up_config', '{\"title\":\"accessKey\",\"componentType\":\"text\",\"value\":\"231YmL0vGAYFTW-rHl4LgA5_\",\"configKey\":\"qnAccessKey\",\"type\":\"qiniu_up_config\"}', 'qnAccessKey', 'LXrRo6YhT22hy5yScLzQJA1QaUGUJ', 0, '2025-06-26 07:42:06.404', '2025-12-05 05:29:36.118', 3, NULL, 0); +INSERT INTO `base_config` VALUES (5, 'qiniu_up_config', '{\"title\":\"SecretKey\",\"componentType\":\"text\",\"value\":\"2daTXFDEG9PuAy4TnvfR1o2\",\"configKey\":\"qnSecretKey\",\"type\":\"qiniu_up_config\"}', 'qnSecretKey', 'BYKKz622111VS-llemF2Hg', 0, '2025-06-26 07:42:06.404', '2025-12-05 05:29:38.346', 3, NULL, 0); INSERT INTO `base_config` VALUES (6, 'qiniu_up_config', '{\"title\":\"存储空间名称\",\"componentType\":\"text\",\"value\":\"hula\",\"configKey\":\"qnStorageName\",\"type\":\"qiniu_up_config\"}', 'qnStorageName', 'hula-spark', 0, '2025-06-26 07:42:06.404', '2025-06-26 07:42:06.494', 3, NULL, 0); INSERT INTO `base_config` VALUES (7, 'qiniu_up_config', '{\"title\":\"七牛云CDN(访问图片用的)\",\"componentType\":\"text\",\"value\":\"https://file.hula.com/\",\"configKey\":\"qnStorageCDN\",\"type\":\"qiniu_up_config\"}', 'qnStorageCDN', 'https://cdn.hulaspark.com', 0, '2025-06-26 07:42:06.404', '2025-06-26 07:42:06.494', 3, NULL, 0); INSERT INTO `base_config` VALUES (8, 'system', '{\"title\":\"大群ID\",\"componentType\":\"text\",\"value\":\"1\",\"configKey\":\"roomGroupId\",\"type\":\"system\"}', 'roomGroupId', '1', 0, '2025-06-26 07:42:06.404', '2025-06-26 07:42:06.494', 3, NULL, 0); @@ -649,6 +649,20 @@ INSERT INTO `base_config` VALUES (13, 'system', '{\"title\":\"AI基础信息\",\ INSERT INTO `base_config` VALUES (14, 'system', '{\"title\":\"AI 扩展配置\",\"componentType\":\"text\",\"value\":\"system/material/20250305/aX3YYCCpDf.png\",\"configKey\":\"extraInfo\",\"type\":\"site_config\"}', 'extraInfo', '{\"ossType\":1,\"smsType\":0}', 0, '2025-06-26 07:42:06.404', '2025-07-16 19:58:23.711', 3, NULL, 0); INSERT INTO `base_config` VALUES (15, 'system', '{\"title\":\"AI AppInfo\",\"componentType\":\"text\",\"value\":\"system/material/20250305/aX3YYCCpDf.png\",\"configKey\":\"appInfo\",\"type\":\"site_config\"}', 'appInfo', '{\"h5Url\":\"https://gpt.panday94.xyz/h5\",\"isSms\":1,\"homeNotice\":\"确保合法合规使用,在运营过程中产生的一切问题后果自负,与作者无关。!\",\"isGptLimit\":0,\"isShare\":1,\"shareRewardNum\":\"20\",\"freeNum\":\"5\",\"isRedemption\":1}', 0, '2025-06-26 07:42:06.404', '2025-07-16 19:58:24.711', 3, NULL, 0); INSERT INTO `base_config` VALUES (16, 'system', '{\"title\":\"AI 微信配置\",\"componentType\":\"text\",\"value\":\"system/material/20250305/aX3YYCCpDf.png\",\"configKey\":\"wxInfo\",\"type\":\"site_config\"}', 'wxInfo', '{\"mpLogin\":0,\"mpPay\":0,\"maAppId\":\"xx\",\"maSecret\":\"xx\",\"mpAppId\":\"xx\",\"mpSecret\":\"xx\",\"mchNo\":\"xx\",\"v3Secret\":\"xx\"}', 0, '2025-06-26 07:42:06.404', '2025-07-16 19:58:26.063', 3, NULL, 0); +INSERT INTO `base_config` VALUES (101, 'minio_up_config', '{\"title\":\"MinIO Endpoint\",\"componentType\":\"text\",\"value\":\"http://127.0.0.1:9000/\",\"configKey\":\"minioEndpoint\",\"type\":\"minio_up_config\"}', 'minioEndpoint', 'https://xxx.hulaspark.com/', 0, '2025-12-04 06:43:49.000', '2025-12-05 05:29:12.656', 3, NULL, 0); +INSERT INTO `base_config` VALUES (102, 'minio_up_config', '{\"title\":\"AccessKey\",\"componentType\":\"text\",\"value\":\"luohuo\",\"configKey\":\"minioAccessKey\",\"type\":\"minio_up_config\"}', 'minioAccessKey', 'mini', 0, '2025-12-04 06:43:49.000', '2025-12-05 05:29:22.288', 3, NULL, 0); +INSERT INTO `base_config` VALUES (103, 'minio_up_config', '{\"title\":\"SecretKey\",\"componentType\":\"text\",\"value\":\"mini.luo123\",\"configKey\":\"minioSecretKey\",\"type\":\"minio_up_config\"}', 'minioSecretKey', 'mini.huo123', 0, '2025-12-04 06:43:49.000', '2025-12-05 05:29:18.384', 3, NULL, 0); +INSERT INTO `base_config` VALUES (104, 'minio_up_config', '{\"title\":\"Bucket\",\"componentType\":\"text\",\"value\":\"dev\",\"configKey\":\"minioBucket\",\"type\":\"minio_up_config\"}', 'minioBucket', 'dev', 0, '2025-12-04 06:43:49.000', '2025-12-04 06:43:49.000', 3, NULL, 0); +INSERT INTO `base_config` VALUES (105, 'minio_up_config', '{\"title\":\"下载前缀(外网可访问)\",\"componentType\":\"text\",\"value\":\"http://127.0.0.1:9000/dev\",\"configKey\":\"minioUrlPrefix\",\"type\":\"minio_up_config\"}', 'minioUrlPrefix', 'https://xxx.hulaspark.com/dev', 0, '2025-12-04 06:43:49.000', '2025-12-05 05:29:06.334', 3, NULL, 0); +INSERT INTO `base_config` VALUES (106, 'youdao_config', '{\"title\":\"有道AppKey\",\"componentType\":\"text\",\"value\":\"\",\"configKey\":\"youdaoAppKey\",\"type\":\"youdao_config\"}', 'youdaoAppKey', '0c41043c0ec229ba3a', 0, '2025-12-04 07:23:36.000', '2025-12-05 05:28:59.556', 3, NULL, 0); +INSERT INTO `base_config` VALUES (107, 'youdao_config', '{\"title\":\"有道AppSecret\",\"componentType\":\"text\",\"value\":\"\",\"configKey\":\"youdaoAppSecret\",\"type\":\"youdao_config\"}', 'youdaoAppSecret', 'CD1Ul6111YF4Z0n4FzsOBPFBSxJ8yD', 0, '2025-12-04 07:23:36.000', '2025-12-05 05:28:42.310', 3, NULL, 0); +INSERT INTO `base_config` VALUES (108, 'tencent_config', '{\"title\":\"腾讯ApiKey\",\"componentType\":\"text\",\"value\":\"\",\"configKey\":\"tencentApiKey\",\"type\":\"tencent_config\"}', 'tencentApiKey', '50i2McgXHpY8e6T3qXMKEzsk1w45zoa', 0, '2025-12-04 07:23:36.000', '2025-12-05 05:28:56.749', 3, NULL, 0); +INSERT INTO `base_config` VALUES (109, 'tencent_config', '{\"title\":\"腾讯SecretId\",\"componentType\":\"text\",\"value\":\"\",\"configKey\":\"tencentSecretId\",\"type\":\"tencent_config\"}', 'tencentSecretId', 'AKI2z0EVlw1zsfHnS2H4nQs8KU2yX0JVV', 0, '2025-12-04 07:23:36.000', '2025-12-05 05:28:54.258', 3, NULL, 0); +INSERT INTO `base_config` VALUES (110, 'tencent_config', '{\"title\":\"腾讯MapKey\",\"componentType\":\"text\",\"value\":\"\",\"configKey\":\"tencentMapKey\",\"type\":\"tencent_config\"}', 'tencentMapKey', 'PF1-34XCC-J6N2D-A3Q3-5K4F4G', 0, '2025-12-04 07:23:36.000', '2025-12-05 05:28:51.944', 3, NULL, 0); +INSERT INTO `base_config` VALUES (111, 'ice_server_config', '{\"title\":\"ICE Server URLs(JSON数组或逗号分隔)\",\"componentType\":\"text\",\"value\":\"[]\",\"configKey\":\"iceServerUrls\",\"type\":\"ice_server_config\"}', 'iceServerUrls', '[]', 0, '2025-12-04 07:23:36.000', '2025-12-04 07:23:36.000', 3, NULL, 0); +INSERT INTO `base_config` VALUES (112, 'ice_server_config', '{\"title\":\"ICE Server Username\",\"componentType\":\"text\",\"value\":\"\",\"configKey\":\"iceServerUsername\",\"type\":\"ice_server_config\"}', 'iceServerUsername', '', 0, '2025-12-04 07:23:36.000', '2025-12-04 07:23:36.000', 3, NULL, 0); +INSERT INTO `base_config` VALUES (113, 'ice_server_config', '{\"title\":\"ICE Server Credential\",\"componentType\":\"text\",\"value\":\"\",\"configKey\":\"iceServerCredential\",\"type\":\"ice_server_config\"}', 'iceServerCredential', '', 0, '2025-12-04 07:23:36.000', '2025-12-04 07:23:36.000', 3, NULL, 0); +INSERT INTO `base_config` VALUES (114, 'translate_config', '{\"title\":\"翻译引擎\",\"componentType\":\"text\",\"value\":\"youdao\",\"configKey\":\"translateDefault\",\"type\":\"translate_config\"}', 'translateDefault', 'youdao', 0, '2025-12-04 10:42:44.000', '2025-12-04 10:42:44.000', 3, NULL, 0); -- ---------------------------- -- Table structure for base_employee @@ -2282,8 +2296,8 @@ CREATE TABLE `def_user` ( -- Records of def_user -- ---------------------------- INSERT INTO `def_user` VALUES (61170828519936, 2, '15147891644', 'HuLa小管家', '', '022', NULL, NULL, '', NULL, b'0', '', '', '1', b'1', '', '2025-08-11 11:11:03.139', '{\"createIp\": \"206.237.119.215\", \"updateIp\": \"120.231.232.41\", \"createIpDetail\": null, \"updateIpDetail\": null}', '2025-12-01 20:30:50', 6, NULL, 'a4d5c225e6709ba025272a31c7e90e0121d5e5ba16695afe0b61370bedb677d0', 'Dawn', '2025-07-07 15:27:02', 1, '2025-03-27 04:23:08', NULL, '2025-07-16 12:26:15', 0, 1); -INSERT INTO `def_user` VALUES (61170828519937, 2, '13275346112', 'Dawn', '2439646234@qq.com', 'https://cdn.hulaspark.com/avatar/2439646234/6ec99d37b8ba1296c325d2d36b46a14d.webp', NULL, NULL, '', NULL, b'0', '', '', '1', b'1', '', '2025-08-11 11:11:03.189', '{\"createIp\": \"206.237.119.215\", \"updateIp\": \"116.24.64.57\", \"createIpDetail\": null, \"updateIpDetail\": {\"ip\": \"116.24.64.57\", \"isp\": \"电信\", \"area\": \"\", \"city\": \"深圳\", \"isp_id\": \"\", \"region\": \"广东省\", \"city_id\": \"\", \"country\": \"中国\", \"region_id\": \"\", \"country_id\": \"\"}}', NULL, 0, NULL, 'a4d5c225e6709ba025272a31c7e90e0121d5e5ba16695afe0b61370bedb677d0', 'Dawn', '2025-12-03 14:44:57', 1, '2025-03-27 04:23:08', NULL, '2025-12-03 14:59:18', 0, 1); -INSERT INTO `def_user` VALUES (61170828529941, 1, '24396462341', 'Dawn', '24396462341@qq.com', 'https://cdn.hulaspark.com/avatar/2439646234/6ec99d37b8ba1296c325d2d36b46a14d.webp', NULL, NULL, '', NULL, b'0', '', '', '1', b'1', '', '2025-08-11 11:11:03.189', '{\"createIp\": \"206.237.119.215\", \"updateIp\": \"116.24.64.57\", \"createIpDetail\": null, \"updateIpDetail\": {\"ip\": \"116.24.64.57\", \"isp\": \"电信\", \"area\": \"\", \"city\": \"深圳\", \"isp_id\": \"\", \"region\": \"广东省\", \"city_id\": \"\", \"country\": \"中国\", \"region_id\": \"\", \"country_id\": \"\"}}', NULL, 0, NULL, 'a4d5c225e6709ba025272a31c7e90e0121d5e5ba16695afe0b61370bedb677d0', 'Dawn', '2025-11-27 12:02:05', 1, '2025-03-27 04:23:08', NULL, '2025-12-02 11:41:54', 0, 1); +INSERT INTO `def_user` VALUES (61170828519937, 2, '13275346112', 'Dawn', '2439646234@qq.com', 'https://cdn.hulaspark.com/avatar/2439646234/6ec99d37b8ba1296c325d2d36b46a14d.webp', NULL, NULL, '', NULL, b'0', '', '', '1', b'1', '', '2025-08-11 11:11:03.189', '{\"createIp\": \"206.237.119.215\", \"updateIp\": \"116.24.64.57\", \"createIpDetail\": null, \"updateIpDetail\": {\"ip\": \"116.24.64.57\", \"isp\": \"电信\", \"area\": \"\", \"city\": \"深圳\", \"isp_id\": \"\", \"region\": \"广东省\", \"city_id\": \"\", \"country\": \"中国\", \"region_id\": \"\", \"country_id\": \"\"}}', NULL, 0, NULL, 'a4d5c225e6709ba025272a31c7e90e0121d5e5ba16695afe0b61370bedb677d0', 'Dawn', '2025-12-05 00:04:51', 1, '2025-03-27 04:23:08', NULL, '2025-12-05 01:04:45', 0, 1); +INSERT INTO `def_user` VALUES (61170828529941, 1, '24396462341', 'Dawn', '24396462341@qq.com', 'https://cdn.hulaspark.com/avatar/2439646234/6ec99d37b8ba1296c325d2d36b46a14d.webp', NULL, NULL, '', NULL, b'0', '', '', '1', b'1', '', '2025-08-11 11:11:03.189', '{\"createIp\": \"206.237.119.215\", \"updateIp\": \"116.24.64.57\", \"createIpDetail\": null, \"updateIpDetail\": {\"ip\": \"116.24.64.57\", \"isp\": \"电信\", \"area\": \"\", \"city\": \"深圳\", \"isp_id\": \"\", \"region\": \"广东省\", \"city_id\": \"\", \"country\": \"中国\", \"region_id\": \"\", \"country_id\": \"\"}}', NULL, 0, NULL, 'a4d5c225e6709ba025272a31c7e90e0121d5e5ba16695afe0b61370bedb677d0', 'Dawn', '2025-12-04 16:17:54', 1, '2025-03-27 04:23:08', NULL, '2025-12-04 17:19:56', 0, 1); -- ---------------------------- -- Table structure for def_user_application @@ -2359,7 +2373,7 @@ CREATE TABLE `extend_interface_log` ( -- Records of extend_interface_log -- ---------------------------- INSERT INTO `extend_interface_log` VALUES (66567882983426, 244439130119864323, '阿里短信', 0, 1, '2025-08-26 16:37:01', '2025-08-26 16:37:00', NULL, '2025-08-26 16:37:00', NULL, 0, 0); -INSERT INTO `extend_interface_log` VALUES (655249535051914248, 244881451621810192, '腾讯邮件', 1619, 65, '2025-12-03 13:38:12', '2025-07-16 18:41:01', NULL, '2025-07-16 18:41:01', NULL, 0, 0); +INSERT INTO `extend_interface_log` VALUES (655249535051914248, 244881451621810192, '腾讯邮件', 1667, 65, '2025-12-05 13:14:22', '2025-07-16 18:41:01', NULL, '2025-07-16 18:41:01', NULL, 0, 0); -- ---------------------------- -- Table structure for extend_interface_logging @@ -2573,7 +2587,7 @@ CREATE TABLE `worker_node` ( `created` timestamp NULL DEFAULT NULL COMMENT '创建时间', `is_del` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1051 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'DB;WorkerID Assigner for UID Generator' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 1061 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'DB;WorkerID Assigner for UID Generator' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of worker_node diff --git a/docs/install/sql/luohuo_im_01.sql b/docs/install/sql/luohuo_im_01.sql index d8c4d3dc..3f9bcfe7 100644 --- a/docs/install/sql/luohuo_im_01.sql +++ b/docs/install/sql/luohuo_im_01.sql @@ -11,7 +11,7 @@ Target Server Version : 80030 (8.0.30) File Encoding : 65001 - Date: 03/12/2025 15:16:13 + Date: 05/12/2025 13:33:06 */ SET NAMES utf8mb4; @@ -35,7 +35,7 @@ CREATE TABLE `base_config` ( `tenant_id` bigint NOT NULL, PRIMARY KEY (`id`) USING BTREE, INDEX `key`(`config_key` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 19 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '参数配置表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 115 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '参数配置表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of base_config @@ -43,8 +43,8 @@ CREATE TABLE `base_config` ( INSERT INTO `base_config` VALUES (1, 'system', '{\"title\":\"系统名称\",\"componentType\":\"text\",\"value\":\"Hula-IM\",\"configKey\":\"systemName\",\"type\":\"system\"}', 'systemName', 'HuLa', 0, '2025-06-26 07:42:06.404', '2025-06-26 07:42:06.494', 3, NULL, 0); INSERT INTO `base_config` VALUES (2, 'system', '{\"title\":\"系统Logo\",\"componentType\":\"text\",\"value\":\"/static/img/Iogo.png\",\"configKey\":\"logo\",\"type\":\"system\"}', 'logo', '/static/img/Iogo.png', 0, '2025-06-26 07:42:06.404', '2025-06-26 07:42:06.494', 3, NULL, 0); INSERT INTO `base_config` VALUES (3, 'qiniu_up_config', '{\"title\":\"空间域名 Domain\",\"componentType\":\"text\",\"value\":\"https://upload-z2.qiniup.com\",\"configKey\":\"qnUploadUrl\",\"type\":\"qiniu_up_config\"}', 'qnUploadUrl', 'https://up-z2.qiniup.com', 0, '2025-06-26 07:42:06.404', '2025-08-18 04:29:56.616', 3, NULL, 0); -INSERT INTO `base_config` VALUES (4, 'qiniu_up_config', '{\"title\":\"accessKey\",\"componentType\":\"text\",\"value\":\"231YmL0vGAYFTW-rHl4LgA5_\",\"configKey\":\"qnAccessKey\",\"type\":\"qiniu_up_config\"}', 'qnAccessKey', 'LXrRo6YhT22hy5yScLzQJAQaUGUJ', 0, '2025-06-26 07:42:06.404', '2025-10-11 03:43:10.679', 3, NULL, 0); -INSERT INTO `base_config` VALUES (5, 'qiniu_up_config', '{\"title\":\"SecretKey\",\"componentType\":\"text\",\"value\":\"2daTXFDEG9PuAy4TnvfR1o2\",\"configKey\":\"qnSecretKey\",\"type\":\"qiniu_up_config\"}', 'qnSecretKey', 'BYKKz6n222111VS-llemF2Hg', 0, '2025-06-26 07:42:06.404', '2025-10-11 03:43:05.090', 3, NULL, 0); +INSERT INTO `base_config` VALUES (4, 'qiniu_up_config', '{\"title\":\"accessKey\",\"componentType\":\"text\",\"value\":\"231YmL0vGAYFTW-rHl4LgA5_\",\"configKey\":\"qnAccessKey\",\"type\":\"qiniu_up_config\"}', 'qnAccessKey', 'LXrRo6YhT22hy5yScLzQJA1QaUGUJ', 0, '2025-06-26 07:42:06.404', '2025-12-05 05:29:36.118', 3, NULL, 0); +INSERT INTO `base_config` VALUES (5, 'qiniu_up_config', '{\"title\":\"SecretKey\",\"componentType\":\"text\",\"value\":\"2daTXFDEG9PuAy4TnvfR1o2\",\"configKey\":\"qnSecretKey\",\"type\":\"qiniu_up_config\"}', 'qnSecretKey', 'BYKKz622111VS-llemF2Hg', 0, '2025-06-26 07:42:06.404', '2025-12-05 05:29:38.346', 3, NULL, 0); INSERT INTO `base_config` VALUES (6, 'qiniu_up_config', '{\"title\":\"存储空间名称\",\"componentType\":\"text\",\"value\":\"hula\",\"configKey\":\"qnStorageName\",\"type\":\"qiniu_up_config\"}', 'qnStorageName', 'hula-spark', 0, '2025-06-26 07:42:06.404', '2025-06-26 07:42:06.494', 3, NULL, 0); INSERT INTO `base_config` VALUES (7, 'qiniu_up_config', '{\"title\":\"七牛云CDN(访问图片用的)\",\"componentType\":\"text\",\"value\":\"https://file.hula.com/\",\"configKey\":\"qnStorageCDN\",\"type\":\"qiniu_up_config\"}', 'qnStorageCDN', 'https://cdn.hulaspark.com', 0, '2025-06-26 07:42:06.404', '2025-06-26 07:42:06.494', 3, NULL, 0); INSERT INTO `base_config` VALUES (8, 'system', '{\"title\":\"大群ID\",\"componentType\":\"text\",\"value\":\"1\",\"configKey\":\"roomGroupId\",\"type\":\"system\"}', 'roomGroupId', '1', 0, '2025-06-26 07:42:06.404', '2025-06-26 07:42:06.494', 3, NULL, 0); @@ -56,6 +56,20 @@ INSERT INTO `base_config` VALUES (13, 'system', '{\"title\":\"AI基础信息\",\ INSERT INTO `base_config` VALUES (14, 'system', '{\"title\":\"AI 扩展配置\",\"componentType\":\"text\",\"value\":\"system/material/20250305/aX3YYCCpDf.png\",\"configKey\":\"extraInfo\",\"type\":\"site_config\"}', 'extraInfo', '{\"ossType\":1,\"smsType\":0}', 0, '2025-06-26 07:42:06.404', '2025-07-16 19:58:23.711', 3, NULL, 0); INSERT INTO `base_config` VALUES (15, 'system', '{\"title\":\"AI AppInfo\",\"componentType\":\"text\",\"value\":\"system/material/20250305/aX3YYCCpDf.png\",\"configKey\":\"appInfo\",\"type\":\"site_config\"}', 'appInfo', '{\"h5Url\":\"https://gpt.panday94.xyz/h5\",\"isSms\":1,\"homeNotice\":\"确保合法合规使用,在运营过程中产生的一切问题后果自负,与作者无关。!\",\"isGptLimit\":0,\"isShare\":1,\"shareRewardNum\":\"20\",\"freeNum\":\"5\",\"isRedemption\":1}', 0, '2025-06-26 07:42:06.404', '2025-07-16 19:58:24.711', 3, NULL, 0); INSERT INTO `base_config` VALUES (16, 'system', '{\"title\":\"AI 微信配置\",\"componentType\":\"text\",\"value\":\"system/material/20250305/aX3YYCCpDf.png\",\"configKey\":\"wxInfo\",\"type\":\"site_config\"}', 'wxInfo', '{\"mpLogin\":0,\"mpPay\":0,\"maAppId\":\"xx\",\"maSecret\":\"xx\",\"mpAppId\":\"xx\",\"mpSecret\":\"xx\",\"mchNo\":\"xx\",\"v3Secret\":\"xx\"}', 0, '2025-06-26 07:42:06.404', '2025-07-16 19:58:26.063', 3, NULL, 0); +INSERT INTO `base_config` VALUES (101, 'minio_up_config', '{\"title\":\"MinIO Endpoint\",\"componentType\":\"text\",\"value\":\"http://127.0.0.1:9000/\",\"configKey\":\"minioEndpoint\",\"type\":\"minio_up_config\"}', 'minioEndpoint', 'https://xxx.hulaspark.com/', 0, '2025-12-04 06:43:49.000', '2025-12-05 05:29:12.656', 3, NULL, 0); +INSERT INTO `base_config` VALUES (102, 'minio_up_config', '{\"title\":\"AccessKey\",\"componentType\":\"text\",\"value\":\"luohuo\",\"configKey\":\"minioAccessKey\",\"type\":\"minio_up_config\"}', 'minioAccessKey', 'mini', 0, '2025-12-04 06:43:49.000', '2025-12-05 05:29:22.288', 3, NULL, 0); +INSERT INTO `base_config` VALUES (103, 'minio_up_config', '{\"title\":\"SecretKey\",\"componentType\":\"text\",\"value\":\"mini.luo123\",\"configKey\":\"minioSecretKey\",\"type\":\"minio_up_config\"}', 'minioSecretKey', 'mini.huo123', 0, '2025-12-04 06:43:49.000', '2025-12-05 05:29:18.384', 3, NULL, 0); +INSERT INTO `base_config` VALUES (104, 'minio_up_config', '{\"title\":\"Bucket\",\"componentType\":\"text\",\"value\":\"dev\",\"configKey\":\"minioBucket\",\"type\":\"minio_up_config\"}', 'minioBucket', 'dev', 0, '2025-12-04 06:43:49.000', '2025-12-04 06:43:49.000', 3, NULL, 0); +INSERT INTO `base_config` VALUES (105, 'minio_up_config', '{\"title\":\"下载前缀(外网可访问)\",\"componentType\":\"text\",\"value\":\"http://127.0.0.1:9000/dev\",\"configKey\":\"minioUrlPrefix\",\"type\":\"minio_up_config\"}', 'minioUrlPrefix', 'https://xxx.hulaspark.com/dev', 0, '2025-12-04 06:43:49.000', '2025-12-05 05:29:06.334', 3, NULL, 0); +INSERT INTO `base_config` VALUES (106, 'youdao_config', '{\"title\":\"有道AppKey\",\"componentType\":\"text\",\"value\":\"\",\"configKey\":\"youdaoAppKey\",\"type\":\"youdao_config\"}', 'youdaoAppKey', '0c41043c0ec229ba3a', 0, '2025-12-04 07:23:36.000', '2025-12-05 05:28:59.556', 3, NULL, 0); +INSERT INTO `base_config` VALUES (107, 'youdao_config', '{\"title\":\"有道AppSecret\",\"componentType\":\"text\",\"value\":\"\",\"configKey\":\"youdaoAppSecret\",\"type\":\"youdao_config\"}', 'youdaoAppSecret', 'CD1Ul6111YF4Z0n4FzsOBPFBSxJ8yD', 0, '2025-12-04 07:23:36.000', '2025-12-05 05:28:42.310', 3, NULL, 0); +INSERT INTO `base_config` VALUES (108, 'tencent_config', '{\"title\":\"腾讯ApiKey\",\"componentType\":\"text\",\"value\":\"\",\"configKey\":\"tencentApiKey\",\"type\":\"tencent_config\"}', 'tencentApiKey', '50i2McgXHpY8e6T3qXMKEzsk1w45zoa', 0, '2025-12-04 07:23:36.000', '2025-12-05 05:28:56.749', 3, NULL, 0); +INSERT INTO `base_config` VALUES (109, 'tencent_config', '{\"title\":\"腾讯SecretId\",\"componentType\":\"text\",\"value\":\"\",\"configKey\":\"tencentSecretId\",\"type\":\"tencent_config\"}', 'tencentSecretId', 'AKI2z0EVlw1zsfHnS2H4nQs8KU2yX0JVV', 0, '2025-12-04 07:23:36.000', '2025-12-05 05:28:54.258', 3, NULL, 0); +INSERT INTO `base_config` VALUES (110, 'tencent_config', '{\"title\":\"腾讯MapKey\",\"componentType\":\"text\",\"value\":\"\",\"configKey\":\"tencentMapKey\",\"type\":\"tencent_config\"}', 'tencentMapKey', 'PF1-34XCC-J6N2D-A3Q3-5K4F4G', 0, '2025-12-04 07:23:36.000', '2025-12-05 05:28:51.944', 3, NULL, 0); +INSERT INTO `base_config` VALUES (111, 'ice_server_config', '{\"title\":\"ICE Server URLs(JSON数组或逗号分隔)\",\"componentType\":\"text\",\"value\":\"[]\",\"configKey\":\"iceServerUrls\",\"type\":\"ice_server_config\"}', 'iceServerUrls', '[]', 0, '2025-12-04 07:23:36.000', '2025-12-04 07:23:36.000', 3, NULL, 0); +INSERT INTO `base_config` VALUES (112, 'ice_server_config', '{\"title\":\"ICE Server Username\",\"componentType\":\"text\",\"value\":\"\",\"configKey\":\"iceServerUsername\",\"type\":\"ice_server_config\"}', 'iceServerUsername', '', 0, '2025-12-04 07:23:36.000', '2025-12-04 07:23:36.000', 3, NULL, 0); +INSERT INTO `base_config` VALUES (113, 'ice_server_config', '{\"title\":\"ICE Server Credential\",\"componentType\":\"text\",\"value\":\"\",\"configKey\":\"iceServerCredential\",\"type\":\"ice_server_config\"}', 'iceServerCredential', '', 0, '2025-12-04 07:23:36.000', '2025-12-04 07:23:36.000', 3, NULL, 0); +INSERT INTO `base_config` VALUES (114, 'translate_config', '{\"title\":\"翻译引擎\",\"componentType\":\"text\",\"value\":\"youdao\",\"configKey\":\"translateDefault\",\"type\":\"translate_config\"}', 'translateDefault', 'youdao', 0, '2025-12-04 10:42:44.000', '2025-12-04 10:42:44.000', 3, NULL, 0); -- ---------------------------- -- Table structure for im_announcements @@ -151,7 +165,7 @@ CREATE TABLE `im_contact` ( INDEX `idx_create_time`(`create_time` ASC) USING BTREE, INDEX `idx_update_time`(`update_time` ASC) USING BTREE, INDEX `idx_contact_room_uid_hide`(`room_id` ASC, `uid` ASC, `hide` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 69082079618161 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会话列表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 69082079619944 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会话列表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of im_contact @@ -324,7 +338,7 @@ CREATE TABLE `im_group_member` ( INDEX `idx_create_time`(`create_time` ASC) USING BTREE, INDEX `idx_update_time`(`update_time` ASC) USING BTREE, INDEX `idx_group_member_uid_isdel_groupid`(`uid` ASC, `is_del` ASC, `group_id` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 102399356298242 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '群成员表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 103117702162434 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '群成员表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of im_group_member @@ -387,7 +401,7 @@ CREATE TABLE `im_message` ( INDEX `idx_from_uid`(`from_uid` ASC) USING BTREE, INDEX `idx_create_time`(`create_time` ASC) USING BTREE, INDEX `idx_update_time`(`update_time` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 102414250271745 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '消息表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 103117702162440 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '消息表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of im_message @@ -417,7 +431,7 @@ CREATE TABLE `im_message_mark` ( INDEX `idx_uid`(`uid` ASC) USING BTREE, INDEX `idx_create_time`(`create_time` ASC) USING BTREE, INDEX `idx_update_time`(`update_time` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 102005775393793 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '消息标记表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 102919517104129 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '消息标记表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of im_message_mark @@ -449,7 +463,7 @@ CREATE TABLE `im_notice` ( INDEX `idx_receiver_type`(`receiver_id` ASC, `event_type` ASC) USING BTREE, INDEX `idx_sender`(`sender_id` ASC) USING BTREE, INDEX `idx_related`(`apply_id` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 102075312759811 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '统一通知表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 102865033095171 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '统一通知表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of im_notice @@ -499,12 +513,12 @@ CREATE TABLE `im_room` ( PRIMARY KEY (`id`) USING BTREE, INDEX `idx_create_time`(`create_time` ASC) USING BTREE, INDEX `idx_update_time`(`update_time` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 102399356298243 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '房间表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 103117702162435 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '房间表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of im_room -- ---------------------------- -INSERT INTO `im_room` VALUES (1, 1, 1, '2025-12-03 14:37:40.514', 102414250271744, NULL, '2024-07-10 11:17:15.521', '2025-12-03 06:37:40.533', 1, 1, NULL, 0); +INSERT INTO `im_room` VALUES (1, 1, 1, '2025-12-05 13:12:56.750', 103117702162439, NULL, '2024-07-10 11:17:15.521', '2025-12-05 05:12:56.772', 1, 1, NULL, 0); -- ---------------------------- -- Table structure for im_room_friend @@ -529,7 +543,7 @@ CREATE TABLE `im_room_friend` ( INDEX `idx_room_id`(`room_id` ASC) USING BTREE, INDEX `idx_create_time`(`create_time` ASC) USING BTREE, INDEX `idx_update_time`(`update_time` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 102399356298244 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '单聊房间表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 103117702162436 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '单聊房间表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of im_room_friend @@ -640,7 +654,7 @@ CREATE TABLE `im_user` ( INDEX `idx_update_time`(`update_time` ASC) USING BTREE, INDEX `idx_active_status_last_opt_time`(`last_opt_time` ASC) USING BTREE, INDEX `account_UNIQUE`(`account` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 102399356298241 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 103117702162433 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of im_user @@ -676,7 +690,7 @@ CREATE TABLE `im_user_apply` ( INDEX `idx_target_id`(`target_id` ASC) USING BTREE, INDEX `idx_create_time`(`create_time` ASC) USING BTREE, INDEX `idx_update_time`(`update_time` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 102075312759809 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户申请表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 102865033095169 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户申请表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of im_user_apply @@ -703,7 +717,7 @@ CREATE TABLE `im_user_backpack` ( INDEX `idx_uid`(`uid` ASC) USING BTREE, INDEX `idx_create_time`(`create_time` ASC) USING BTREE, INDEX `idx_update_time`(`update_time` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 102399356298251 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户背包表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 103117702162443 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户背包表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of im_user_backpack @@ -746,7 +760,7 @@ CREATE TABLE `im_user_emoji` ( `update_by` bigint NULL DEFAULT NULL COMMENT '更新者', PRIMARY KEY (`id`) USING BTREE, INDEX `IDX_USER_EMOJIS_UID`(`uid` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 102122003751937 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户表情包' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 102499101041665 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户表情包' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of im_user_emoji @@ -779,7 +793,7 @@ CREATE TABLE `im_user_friend` ( INDEX `idx_uid_friend_uid`(`uid` ASC, `friend_uid` ASC) USING BTREE, INDEX `idx_create_time`(`create_time` ASC) USING BTREE, INDEX `idx_update_time`(`update_time` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 102399356298246 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户联系人表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 103117702162438 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户联系人表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of im_user_friend @@ -957,7 +971,7 @@ CREATE TABLE `secure_invoke_record` ( `is_del` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_next_retry_time`(`next_retry_time` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 102414250271746 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '本地消息表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 103117702162442 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '本地消息表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of secure_invoke_record @@ -976,7 +990,7 @@ CREATE TABLE `worker_node` ( `modified` timestamp NULL DEFAULT NULL COMMENT '修改时间', `created` timestamp NULL DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 243 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'DB;WorkerID Assigner for UID Generator' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 245 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'DB;WorkerID Assigner for UID Generator' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of worker_node diff --git a/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/deepseek/DeepSeekSseClient.java b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/deepseek/DeepSeekSseClient.java new file mode 100644 index 00000000..1da271ed --- /dev/null +++ b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/deepseek/DeepSeekSseClient.java @@ -0,0 +1,114 @@ +package com.luohuo.flex.ai.core.model.deepseek; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSink; + +import java.io.BufferedReader; +import java.io.InputStream; +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.HashMap; +import java.util.List; +import java.util.Map; + +public class DeepSeekSseClient { + + private final String baseUrl; + private final String apiKey; + private final ObjectMapper mapper = new ObjectMapper(); + + public DeepSeekSseClient(String baseUrl, String apiKey) { + this.baseUrl = baseUrl == null || baseUrl.isBlank() ? "https://api.deepseek.com" : baseUrl; + this.apiKey = apiKey; + } + + public static class Chunk { + public String content; + public String reasoning; + public boolean done; + } + + public Flux streamChat(String model, + List> messages, + Integer maxTokens, + Double temperature) { + return Flux.create(sink -> { + try { + Map body = new HashMap<>(); + body.put("model", model); + body.put("messages", messages); + body.put("stream", true); + if (maxTokens != null) body.put("max_tokens", maxTokens); + if (temperature != null) body.put("temperature", temperature); + + String json = mapper.writeValueAsString(body); + + HttpClient client = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(20)) + .build(); + URI uri = URI.create(baseUrl.endsWith("/") ? baseUrl + "v1/chat/completions" : baseUrl + "/v1/chat/completions"); + HttpRequest request = HttpRequest.newBuilder(uri) + .header("Authorization", "Bearer " + apiKey) + .header("Content-Type", "application/json") + .header("Accept", "text/event-stream") + .POST(HttpRequest.BodyPublishers.ofString(json)) + .build(); + + client.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) + .thenAccept(response -> { + int status = response.statusCode(); + if (status < 200 || status >= 300) { + sink.error(new IllegalStateException("DeepSeek SSE HTTP status: " + status)); + return; + } + try (InputStream is = response.body(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + if (!line.startsWith("data:")) continue; + String payload = line.substring(5).trim(); + if (payload.equals("[DONE]")) { + Chunk c = new Chunk(); + c.done = true; + sink.next(c); + sink.complete(); + break; + } + try { + JsonNode root = mapper.readTree(payload); + JsonNode choices = root.path("choices"); + if (choices.isArray() && choices.size() > 0) { + JsonNode delta = choices.get(0).path("delta"); + String content = delta.path("content").isMissingNode() ? null : delta.path("content").asText(null); + String reasoning = delta.path("reasoning_content").isMissingNode() ? null : delta.path("reasoning_content").asText(null); + Chunk c = new Chunk(); + c.content = content; + c.reasoning = reasoning; + sink.next(c); + } + } catch (Exception e) { + sink.error(e); + break; + } + } + } catch (Exception e) { + sink.error(e); + } + }) + .exceptionally(e -> { + sink.error(e); + return null; + }); + } catch (Exception e) { + sink.error(e); + } + }, FluxSink.OverflowStrategy.BUFFER); + } +} diff --git a/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/openai/OpenAiCompatClient.java b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/openai/OpenAiCompatClient.java new file mode 100644 index 00000000..174fc762 --- /dev/null +++ b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/openai/OpenAiCompatClient.java @@ -0,0 +1,80 @@ +package com.luohuo.flex.ai.core.model.openai; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 通用 OpenAI 兼容 同步客户端(非流式) + * 读取单次返回中的 message.content 与 message.reasoning_content + */ +public class OpenAiCompatClient { + + private final String baseUrl; + private final String apiKey; + private final ObjectMapper mapper = new ObjectMapper(); + + public OpenAiCompatClient(String baseUrl, String apiKey) { + this.baseUrl = baseUrl == null || baseUrl.isBlank() ? "https://api.openai.com" : baseUrl; + this.apiKey = apiKey; + } + + public static class Result { + public String content; + public String reasoning; + } + + public Result callChat(String model, + List> messages, + Integer maxTokens, + Double temperature) throws Exception { + Map body = new HashMap<>(); + body.put("model", model); + body.put("messages", messages); + if (maxTokens != null) body.put("max_tokens", maxTokens); + if (temperature != null) body.put("temperature", temperature); + + String json = mapper.writeValueAsString(body); + + HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(20)).build(); + URI uri = URI.create(baseUrl.endsWith("/") ? baseUrl + "v1/chat/completions" : baseUrl + "/v1/chat/completions"); + HttpRequest request = HttpRequest.newBuilder(uri) + .header("Authorization", "Bearer " + apiKey) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(json)) + .build(); + + HttpResponse resp = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (resp.statusCode() < 200 || resp.statusCode() >= 300) { + throw new IllegalStateException("OpenAI-Compat HTTP status: " + resp.statusCode()); + } + JsonNode root = mapper.readTree(resp.body()); + JsonNode choices = root.path("choices"); + Result r = new Result(); + if (choices.isArray() && choices.size() > 0) { + JsonNode msg = choices.get(0).path("message"); + r.content = msg.path("content").isMissingNode() ? null : msg.path("content").asText(null); + // 兼容不同命名 + if (!msg.path("reasoning_content").isMissingNode()) { + r.reasoning = msg.path("reasoning_content").asText(null); + } else if (!msg.path("reasoning").isMissingNode()) { + r.reasoning = msg.path("reasoning").asText(null); + } else if (!msg.path("thinking").isMissingNode()) { + r.reasoning = msg.path("thinking").asText(null); + } else if (!msg.path("deliberate_output").isMissingNode()) { + r.reasoning = msg.path("deliberate_output").asText(null); + } else if (!msg.path("thoughts").isMissingNode()) { + r.reasoning = msg.path("thoughts").asText(null); + } + } + return r; + } +} diff --git a/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/openai/OpenAiCompatSseClient.java b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/openai/OpenAiCompatSseClient.java new file mode 100644 index 00000000..67338b6d --- /dev/null +++ b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/openai/OpenAiCompatSseClient.java @@ -0,0 +1,130 @@ +package com.luohuo.flex.ai.core.model.openai; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSink; + +import java.io.BufferedReader; +import java.io.InputStream; +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.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 通用 OpenAI 兼容 SSE 客户端 + * 可用于 Gitee AI、SiliconFlow、OpenAI、OpenRouter 等提供 OpenAI 兼容接口的平台 + */ +public class OpenAiCompatSseClient { + + private final String baseUrl; + private final String apiKey; + private final ObjectMapper mapper = new ObjectMapper(); + + public OpenAiCompatSseClient(String baseUrl, String apiKey) { + this.baseUrl = baseUrl == null || baseUrl.isBlank() ? "https://api.openai.com" : baseUrl; + this.apiKey = apiKey; + } + + public static class Chunk { + public String content; + public String reasoning; + public boolean done; + } + + public Flux streamChat(String model, + List> messages, + Integer maxTokens, + Double temperature) { + return Flux.create(sink -> { + try { + Map body = new HashMap<>(); + body.put("model", model); + body.put("messages", messages); + body.put("stream", true); + if (maxTokens != null) body.put("max_tokens", maxTokens); + if (temperature != null) body.put("temperature", temperature); + + String json = mapper.writeValueAsString(body); + + HttpClient client = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(20)) + .build(); + URI uri = URI.create(baseUrl.endsWith("/") ? baseUrl + "v1/chat/completions" : baseUrl + "/v1/chat/completions"); + HttpRequest request = HttpRequest.newBuilder(uri) + .header("Authorization", "Bearer " + apiKey) + .header("Content-Type", "application/json") + .header("Accept", "text/event-stream") + .POST(HttpRequest.BodyPublishers.ofString(json)) + .build(); + + client.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) + .thenAccept(response -> { + int status = response.statusCode(); + if (status < 200 || status >= 300) { + sink.error(new IllegalStateException("OpenAI-Compat SSE HTTP status: " + status)); + return; + } + try (InputStream is = response.body(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + if (!line.startsWith("data:")) continue; + String payload = line.substring(5).trim(); + if (payload.equals("[DONE]")) { + Chunk c = new Chunk(); + c.done = true; + sink.next(c); + sink.complete(); + break; + } + try { + JsonNode root = mapper.readTree(payload); + JsonNode choices = root.path("choices"); + if (choices.isArray() && choices.size() > 0) { + JsonNode delta = choices.get(0).path("delta"); + String content = delta.path("content").isMissingNode() ? null : delta.path("content").asText(null); + // 兼容不同平台的推理字段命名 + String reasoning = null; + if (!delta.path("reasoning_content").isMissingNode()) { + reasoning = delta.path("reasoning_content").asText(null); + } else if (!delta.path("reasoning").isMissingNode()) { + reasoning = delta.path("reasoning").asText(null); + } else if (!delta.path("thinking").isMissingNode()) { + reasoning = delta.path("thinking").asText(null); + } else if (!delta.path("deliberate_output").isMissingNode()) { + reasoning = delta.path("deliberate_output").asText(null); + } else if (!delta.path("thoughts").isMissingNode()) { + reasoning = delta.path("thoughts").asText(null); + } + Chunk c = new Chunk(); + c.content = content; + c.reasoning = reasoning; + sink.next(c); + } + } catch (Exception e) { + sink.error(e); + break; + } + } + } catch (Exception e) { + sink.error(e); + } + }) + .exceptionally(e -> { + sink.error(e); + return null; + }); + } catch (Exception e) { + sink.error(e); + } + }, FluxSink.OverflowStrategy.BUFFER); + } +} diff --git a/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/ChatCallStrategy.java b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/ChatCallStrategy.java new file mode 100644 index 00000000..665833e7 --- /dev/null +++ b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/ChatCallStrategy.java @@ -0,0 +1,13 @@ +package com.luohuo.flex.ai.core.model.strategy.chat; + +import java.util.List; +import java.util.Map; + +public interface ChatCallStrategy { + class Result { + public String content; + public String reasoning; + } + + Result call(String baseUrl, String apiKey, String model, List> messages, Integer maxTokens, Double temperature) throws Exception; +} diff --git a/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/ChatStrategyFactory.java b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/ChatStrategyFactory.java new file mode 100644 index 00000000..b783d678 --- /dev/null +++ b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/ChatStrategyFactory.java @@ -0,0 +1,44 @@ +package com.luohuo.flex.ai.core.model.strategy.chat; + +import com.luohuo.flex.ai.enums.AiPlatformEnum; +import cn.hutool.core.util.StrUtil; +import com.luohuo.flex.ai.core.model.openrouter.OpenRouterApiConstants; +import com.luohuo.flex.ai.core.model.silicon.SiliconFlowApiConstants; + +public class ChatStrategyFactory { + public static ChatStreamingStrategy getStreamingStrategy(AiPlatformEnum platform) { + if (platform == AiPlatformEnum.DEEP_SEEK) { + return new DeepSeekStreamingStrategy(); + } + if (platform == AiPlatformEnum.GITEE_AI || platform == AiPlatformEnum.SILICON_FLOW || platform == AiPlatformEnum.OPENAI || platform == AiPlatformEnum.OPENROUTER) { + return new OpenAiCompatStreamingStrategy(); + } + return null; + } + + public static ChatCallStrategy getCallStrategy(AiPlatformEnum platform) { + if (platform == AiPlatformEnum.DEEP_SEEK || platform == AiPlatformEnum.GITEE_AI || platform == AiPlatformEnum.SILICON_FLOW || platform == AiPlatformEnum.OPENAI || platform == AiPlatformEnum.OPENROUTER) { + return new OpenAiCompatCallStrategy(); + } + return null; + } + + public static String resolveBaseUrl(AiPlatformEnum platform, String apiKeyUrl) { + if (StrUtil.isNotBlank(apiKeyUrl)) return apiKeyUrl; + if (platform == AiPlatformEnum.DEEP_SEEK) return "https://api.deepseek.com"; + if (platform == AiPlatformEnum.GITEE_AI) return "https://ai.gitee.com"; + if (platform == AiPlatformEnum.SILICON_FLOW) return SiliconFlowApiConstants.DEFAULT_BASE_URL; + if (platform == AiPlatformEnum.OPENROUTER) return OpenRouterApiConstants.DEFAULT_BASE_URL; + return "https://api.openai.com"; + } + + public static String normalizeModel(AiPlatformEnum platform, String model, Boolean reasoningEnabled) { + String effective = model; + if (Boolean.TRUE.equals(reasoningEnabled)) { + if (platform == AiPlatformEnum.DEEP_SEEK || platform == AiPlatformEnum.HUN_YUAN && StrUtil.startWithIgnoreCase(effective, "deepseek")) { + effective = "deepseek-reasoner"; + } + } + return effective; + } +} diff --git a/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/ChatStreamingStrategy.java b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/ChatStreamingStrategy.java new file mode 100644 index 00000000..e130ca72 --- /dev/null +++ b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/ChatStreamingStrategy.java @@ -0,0 +1,10 @@ +package com.luohuo.flex.ai.core.model.strategy.chat; + +import reactor.core.publisher.Flux; + +import java.util.List; +import java.util.Map; + +public interface ChatStreamingStrategy { + Flux stream(String baseUrl, String apiKey, String model, List> messages, Integer maxTokens, Double temperature); +} diff --git a/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/DeepSeekStreamingStrategy.java b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/DeepSeekStreamingStrategy.java new file mode 100644 index 00000000..b8c176f2 --- /dev/null +++ b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/DeepSeekStreamingStrategy.java @@ -0,0 +1,20 @@ +package com.luohuo.flex.ai.core.model.strategy.chat; + +import com.luohuo.flex.ai.core.model.deepseek.DeepSeekSseClient; +import reactor.core.publisher.Flux; + +import java.util.List; +import java.util.Map; + +public class DeepSeekStreamingStrategy implements ChatStreamingStrategy { + @Override + public Flux stream(String baseUrl, String apiKey, String model, List> messages, Integer maxTokens, Double temperature) { + DeepSeekSseClient client = new DeepSeekSseClient(baseUrl, apiKey); + return client.streamChat(model, messages, maxTokens, temperature).map(c -> { + ReasoningChunk r = new ReasoningChunk(); + r.content = c.content; + r.reasoning = c.reasoning; + return r; + }); + } +} diff --git a/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/OpenAiCompatCallStrategy.java b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/OpenAiCompatCallStrategy.java new file mode 100644 index 00000000..c35cd0fc --- /dev/null +++ b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/OpenAiCompatCallStrategy.java @@ -0,0 +1,18 @@ +package com.luohuo.flex.ai.core.model.strategy.chat; + +import com.luohuo.flex.ai.core.model.openai.OpenAiCompatClient; + +import java.util.List; +import java.util.Map; + +public class OpenAiCompatCallStrategy implements ChatCallStrategy { + @Override + public Result call(String baseUrl, String apiKey, String model, List> messages, Integer maxTokens, Double temperature) throws Exception { + OpenAiCompatClient client = new OpenAiCompatClient(baseUrl, apiKey); + OpenAiCompatClient.Result res = client.callChat(model, messages, maxTokens, temperature); + Result r = new Result(); + r.content = res.content; + r.reasoning = res.reasoning; + return r; + } +} diff --git a/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/OpenAiCompatStreamingStrategy.java b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/OpenAiCompatStreamingStrategy.java new file mode 100644 index 00000000..6049cae5 --- /dev/null +++ b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/OpenAiCompatStreamingStrategy.java @@ -0,0 +1,20 @@ +package com.luohuo.flex.ai.core.model.strategy.chat; + +import com.luohuo.flex.ai.core.model.openai.OpenAiCompatSseClient; +import reactor.core.publisher.Flux; + +import java.util.List; +import java.util.Map; + +public class OpenAiCompatStreamingStrategy implements ChatStreamingStrategy { + @Override + public Flux stream(String baseUrl, String apiKey, String model, List> messages, Integer maxTokens, Double temperature) { + OpenAiCompatSseClient client = new OpenAiCompatSseClient(baseUrl, apiKey); + return client.streamChat(model, messages, maxTokens, temperature).map(c -> { + ReasoningChunk r = new ReasoningChunk(); + r.content = c.content; + r.reasoning = c.reasoning; + return r; + }); + } +} diff --git a/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/ReasoningChunk.java b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/ReasoningChunk.java new file mode 100644 index 00000000..80034b59 --- /dev/null +++ b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/core/model/strategy/chat/ReasoningChunk.java @@ -0,0 +1,6 @@ +package com.luohuo.flex.ai.core.model.strategy.chat; + +public class ReasoningChunk { + public String content; + public String reasoning; +} diff --git a/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/service/chat/AiChatMessageServiceImpl.java b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/service/chat/AiChatMessageServiceImpl.java index f6fc556c..12990abf 100644 --- a/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/service/chat/AiChatMessageServiceImpl.java +++ b/luohuo-cloud/luohuo-ai/luohuo-ai-biz/src/main/java/com/luohuo/flex/ai/service/chat/AiChatMessageServiceImpl.java @@ -11,6 +11,11 @@ import com.luohuo.flex.ai.controller.chat.vo.message.AiChatMessagePageReqVO; import com.luohuo.flex.ai.controller.chat.vo.message.AiChatMessageRespVO; import com.luohuo.flex.ai.controller.chat.vo.message.AiChatMessageSendReqVO; import com.luohuo.flex.ai.controller.chat.vo.message.AiChatMessageSendRespVO; + +import com.luohuo.flex.ai.core.model.strategy.chat.ChatCallStrategy; +import com.luohuo.flex.ai.core.model.strategy.chat.ChatStrategyFactory; +import com.luohuo.flex.ai.core.model.strategy.chat.ChatStreamingStrategy; +import com.luohuo.flex.ai.core.model.strategy.chat.ReasoningChunk; import com.luohuo.flex.ai.dal.chat.AiChatConversationDO; import com.luohuo.flex.ai.dal.chat.AiChatMessageDO; import com.luohuo.flex.ai.dal.knowledge.AiKnowledgeDocumentDO; @@ -28,6 +33,11 @@ import com.luohuo.flex.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO; import com.luohuo.flex.ai.service.model.AiChatRoleService; import com.luohuo.flex.ai.service.model.AiModelService; import com.luohuo.flex.ai.service.model.AiModelUsageService; +import com.luohuo.flex.ai.service.model.AiApiKeyService; +import com.luohuo.flex.ai.dal.model.AiApiKeyDO; + +import com.luohuo.flex.ai.core.model.openrouter.OpenRouterApiConstants; +import com.luohuo.flex.ai.core.model.silicon.SiliconFlowApiConstants; import com.luohuo.flex.ai.service.model.AiToolService; import com.luohuo.flex.ai.utils.AiUtils; import com.luohuo.flex.ai.utils.BeanUtils; @@ -38,6 +48,7 @@ import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.MessageType; import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.metadata.ChatResponseMetadata; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.StreamingChatModel; @@ -98,6 +109,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { private AiToolService toolService; @Resource private AiModelUsageService modelUsageService; + @Resource + private AiApiKeyService apiKeyService; @Transactional(rollbackFor = Exception.class) public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) { @@ -137,19 +150,51 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { // 3.2 创建 chat 需要的 Prompt Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, model, sendReqVO); - ChatResponse chatResponse = chatModel.call(prompt); - - // 3.3 更新响应内容 - String newContent = chatResponse.getResult().getOutput().getText(); - chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(newContent)); + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); + String answerContent; + String reasoningContent; + ChatCallStrategy callStrategy = ChatStrategyFactory.getCallStrategy(platform); + if (callStrategy != null) { + AiApiKeyDO apiKey = apiKeyService.validateApiKey(model.getKeyId()); + String baseUrl = ChatStrategyFactory.resolveBaseUrl(platform, apiKey.getUrl()); + String effectiveModel = ChatStrategyFactory.normalizeModel(platform, model.getModel(), sendReqVO.getReasoningEnabled()); + List instructions = prompt.getInstructions(); + List> messages = new ArrayList<>(); + for (Message m : instructions) { + String role; + if (m instanceof SystemMessage) role = "system"; else if (m instanceof UserMessage) role = "user"; else role = "assistant"; + Map item = Map.of("role", role, "content", m.getText()); + messages.add(item); + } + try { + ChatCallStrategy.Result result = callStrategy.call(baseUrl, apiKey.getApiKey(), effectiveModel, messages, conversation.getMaxTokens(), conversation.getTemperature()); + answerContent = stripReasoningTags(StrUtil.nullToDefault(result.content, "")); + reasoningContent = Boolean.TRUE.equals(sendReqVO.getReasoningEnabled()) ? StrUtil.nullToDefault(result.reasoning, "") : ""; + } catch (Exception e) { + ChatResponse chatResponse = chatModel.call(prompt); + String rawContentFallback = chatResponse.getResult().getOutput().getText(); + answerContent = stripReasoningTags(StrUtil.nullToDefault(rawContentFallback, "")); + reasoningContent = Boolean.TRUE.equals(sendReqVO.getReasoningEnabled()) ? extractReasoningFromText(rawContentFallback) : null; + } + } else { + ChatResponse chatResponse = chatModel.call(prompt); + String rawContent = chatResponse.getResult().getOutput().getText(); + answerContent = stripReasoningTags(StrUtil.nullToDefault(rawContent, "")); + reasoningContent = Boolean.TRUE.equals(sendReqVO.getReasoningEnabled()) ? extractReasoningFromText(rawContent) : null; + } + AiChatMessageDO updateMessage = new AiChatMessageDO().setId(assistantMessage.getId()).setContent(answerContent); + if (Boolean.TRUE.equals(sendReqVO.getReasoningEnabled()) && StrUtil.isNotEmpty(reasoningContent)) { + updateMessage.setReasoningContent(reasoningContent); + } + chatMessageMapper.updateById(updateMessage); // 用量统计:prompt + completion - int promptTokens = estimateContextTokens(conversation, historyMessages, sendReqVO.getContent()); - int completionTokens = estimateTokens(newContent); + int promptTokens = estimateContextTokens(conversation, historyMessages, sendReqVO.getContent()); + int completionTokens = estimateTokens(answerContent); int newTotal = (conversation.getTokenUsage() == null ? 0 : conversation.getTokenUsage()) + promptTokens + completionTokens; - AiChatConversationUpdateMyReqVO updateReq = new AiChatConversationUpdateMyReqVO(); - updateReq.setId(conversation.getId()); - updateReq.setTokenUsage(newTotal); - chatConversationService.updateChatConversationMy(updateReq, userId); + AiChatConversationUpdateMyReqVO updateReq = new AiChatConversationUpdateMyReqVO(); + updateReq.setId(conversation.getId()); + updateReq.setTokenUsage(newTotal); + chatConversationService.updateChatConversationMy(updateReq, userId); // 3.4 响应结果 Map documentMap = knowledgeDocumentService.getKnowledgeDocumentMap( convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId)); @@ -161,7 +206,9 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { return new AiChatMessageSendRespVO() .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) - .setContent(newContent).setSegments(segments)); + .setContent(answerContent) + .setReasoningContent(reasoningContent) + .setSegments(segments)); } @Override @@ -205,73 +252,230 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { // 4.2 构建 Prompt,并进行调用 Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, model, sendReqVO); + + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); + ChatStreamingStrategy streamingStrategy = ChatStrategyFactory.getStreamingStrategy(platform); + if (streamingStrategy != null) { + StringBuffer contentBuffer = new StringBuffer(); + StringBuffer reasoningBuffer = new StringBuffer(); + AiApiKeyDO apiKey = apiKeyService.validateApiKey(model.getKeyId()); + String baseUrl = ChatStrategyFactory.resolveBaseUrl(platform, apiKey.getUrl()); + String effectiveModel = ChatStrategyFactory.normalizeModel(platform, model.getModel(), sendReqVO.getReasoningEnabled()); + List instructions = prompt.getInstructions(); + List> messages = new ArrayList<>(); + for (Message m : instructions) { + String role; + if (m instanceof SystemMessage) role = "system"; else if (m instanceof UserMessage) role = "user"; else role = "assistant"; + Map item = Map.of("role", role, "content", m.getText()); + messages.add(item); + } + Flux flux = streamingStrategy.stream(baseUrl, apiKey.getApiKey(), effectiveModel, messages, conversation.getMaxTokens(), conversation.getTemperature()); + final boolean reasoningOn = sendReqVO.getReasoningEnabled(); + return flux.map(obj -> { + String incremental = StrUtil.nullToDefault(obj.content, ""); + String reasoningInc = reasoningOn ? StrUtil.nullToDefault(obj.reasoning, "") : ""; + List segments = null; + if (StrUtil.isEmpty(contentBuffer)) { + Map documentMap = knowledgeDocumentService.getKnowledgeDocumentMap( + convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId)); + segments = BeanUtils.toBean(knowledgeSegments, AiChatMessageRespVO.KnowledgeSegment.class, segment -> { + AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId()); + segment.setDocumentName(document != null ? document.getName() : null); + }); + } + String answerChunk = stripReasoningTags(incremental); + contentBuffer.append(answerChunk); + String newReasoningContent = reasoningInc; + if (reasoningOn && StrUtil.isNotEmpty(newReasoningContent)) reasoningBuffer.append(newReasoningContent); + return success(new AiChatMessageSendRespVO() + .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) + .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) + .setContent(answerChunk) + .setReasoningContent(reasoningOn && StrUtil.isNotEmpty(newReasoningContent) ? newReasoningContent : null) + .setSegments(segments))); + }).doOnComplete(() -> { + try { + ContextUtil.setIgnore(true); + String content = stripReasoningTags(contentBuffer.toString()); + String reasoningContent = reasoningBuffer.toString(); + AiChatMessageDO updateMessage = new AiChatMessageDO().setId(assistantMessage.getId()).setContent(content); + if (reasoningOn && StrUtil.isNotEmpty(reasoningContent)) { + updateMessage.setReasoningContent(reasoningContent); + } + chatMessageMapper.updateById(updateMessage); + int promptTokens = estimateContextTokens(conversation, historyMessages, sendReqVO.getContent()); + int completionTokens = estimateTokens(content); + int newTotal = (conversation.getTokenUsage() == null ? 0 : conversation.getTokenUsage()) + promptTokens + completionTokens; + AiChatConversationUpdateMyReqVO streamUpdateReq = new AiChatConversationUpdateMyReqVO(); + streamUpdateReq.setId(conversation.getId()); + streamUpdateReq.setTokenUsage(newTotal); + chatConversationService.updateChatConversationMy(streamUpdateReq, conversation.getUserId()); + } finally { + ContextUtil.setIgnore(false); + } + }).doOnError(throwable -> log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable)) + .onErrorResume(error -> { + Flux streamResponse = chatModel.stream(prompt); + StringBuffer fbContentBuffer = new StringBuffer(); + StringBuffer fbReasoningBuffer = new StringBuffer(); + return streamResponse.map(chunk -> { + List segments = null; + if (StrUtil.isEmpty(fbContentBuffer)) { + Map documentMap = knowledgeDocumentService.getKnowledgeDocumentMap( + convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId)); + segments = BeanUtils.toBean(knowledgeSegments, AiChatMessageRespVO.KnowledgeSegment.class, segment -> { + AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId()); + segment.setDocumentName(document != null ? document.getName() : null); + }); + } + String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getText() : null; + newContent = StrUtil.nullToDefault(newContent, ""); + String answerChunk = stripReasoningTags(newContent); + fbContentBuffer.append(answerChunk); + String newReasoningContent = null; + if (reasoningOn && chunk.getMetadata() != null) { + String metaReason = extractReasoningFromMetadata(chunk.getMetadata()); + if (StrUtil.isNotEmpty(metaReason)) { + newReasoningContent = metaReason; + fbReasoningBuffer.append(metaReason); + } else { + String extracted = extractReasoningFromText(newContent); + if (StrUtil.isNotEmpty(extracted)) { + newReasoningContent = extracted; + fbReasoningBuffer.append(extracted); + } + } + } else if (reasoningOn) { + String extracted = extractReasoningFromText(newContent); + if (StrUtil.isNotEmpty(extracted)) { + newReasoningContent = extracted; + fbReasoningBuffer.append(extracted); + } + } + return success(new AiChatMessageSendRespVO() + .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) + .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) + .setContent(answerChunk) + .setReasoningContent(reasoningOn ? newReasoningContent : null) + .setSegments(segments))); + }).doOnComplete(() -> { + try { + ContextUtil.setIgnore(true); + String content = stripReasoningTags(fbContentBuffer.toString()); + String reasoningContent = fbReasoningBuffer.toString(); + AiChatMessageDO updateMessage = new AiChatMessageDO().setId(assistantMessage.getId()).setContent(content); + if (reasoningOn && StrUtil.isNotEmpty(reasoningContent)) { + updateMessage.setReasoningContent(reasoningContent); + } + chatMessageMapper.updateById(updateMessage); + int promptTokens = estimateContextTokens(conversation, historyMessages, sendReqVO.getContent()); + int completionTokens = estimateTokens(content); + int newTotal = (conversation.getTokenUsage() == null ? 0 : conversation.getTokenUsage()) + promptTokens + completionTokens; + AiChatConversationUpdateMyReqVO streamUpdateReq = new AiChatConversationUpdateMyReqVO(); + streamUpdateReq.setId(conversation.getId()); + streamUpdateReq.setTokenUsage(newTotal); + chatConversationService.updateChatConversationMy(streamUpdateReq, conversation.getUserId()); + } finally { + ContextUtil.setIgnore(false); + } + }); + }); + } + Flux streamResponse = chatModel.stream(prompt); - // 4.3 流式返回 StringBuffer contentBuffer = new StringBuffer(); - StringBuffer reasoningBuffer = new StringBuffer(); // 用于累积推理内容 + StringBuffer reasoningBuffer = new StringBuffer(); return streamResponse.map(chunk -> { - // 处理知识库的返回,只有首次才有 - List segments = null; - if (StrUtil.isEmpty(contentBuffer)) { - Map documentMap = knowledgeDocumentService.getKnowledgeDocumentMap( - convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId)); - segments = BeanUtils.toBean(knowledgeSegments, AiChatMessageRespVO.KnowledgeSegment.class, segment -> { - AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId()); - segment.setDocumentName(document != null ? document.getName() : null); - }); + // 处理知识库的返回,只有首次才有 + List segments = null; + if (StrUtil.isEmpty(contentBuffer)) { + Map documentMap = knowledgeDocumentService.getKnowledgeDocumentMap( + convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId)); + segments = BeanUtils.toBean(knowledgeSegments, AiChatMessageRespVO.KnowledgeSegment.class, segment -> { + AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId()); + segment.setDocumentName(document != null ? document.getName() : null); + }); + } + + // 响应结果 + String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getText() : null; + newContent = StrUtil.nullToDefault(newContent, ""); + String answerChunk = stripReasoningTags(newContent); + contentBuffer.append(answerChunk); + + // 提取推理内容 + String newReasoningContent = null; + if (chunk.getMetadata() != null) { + String metaReason = extractReasoningFromMetadata(chunk.getMetadata()); + if (StrUtil.isNotEmpty(metaReason)) { + newReasoningContent = metaReason; + reasoningBuffer.append(metaReason); + } else { + String extracted = extractReasoningFromText(newContent); + if (StrUtil.isNotEmpty(extracted)) { + newReasoningContent = extracted; + reasoningBuffer.append(extracted); + } + } + } else { + String extracted = extractReasoningFromText(newContent); + if (StrUtil.isNotEmpty(extracted)) { + newReasoningContent = extracted; + reasoningBuffer.append(extracted); + } + } + + return success(new AiChatMessageSendRespVO() + .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) + .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) + .setContent(answerChunk) + .setReasoningContent(newReasoningContent) + .setSegments(segments))); + }).doOnComplete(() -> { + // 手动设置租户信息(因为 Flux 异步会切换线程,导致 ThreadLocal 丢失) + try { + ContextUtil.setIgnore(true); + + String content = stripReasoningTags(contentBuffer.toString()); + String reasoningContent = reasoningBuffer.toString(); + AiChatMessageDO updateMessage = new AiChatMessageDO().setId(assistantMessage.getId()).setContent(content); + if (StrUtil.isNotEmpty(reasoningContent)) { + updateMessage.setReasoningContent(reasoningContent); + } + chatMessageMapper.updateById(updateMessage); + // 用量统计:prompt + completion(流式完成时) + int promptTokens = estimateContextTokens(conversation, historyMessages, sendReqVO.getContent()); + int completionTokens = estimateTokens(content); + int newTotal = (conversation.getTokenUsage() == null ? 0 : conversation.getTokenUsage()) + promptTokens + completionTokens; + AiChatConversationUpdateMyReqVO streamUpdateReq = new AiChatConversationUpdateMyReqVO(); + streamUpdateReq.setId(conversation.getId()); + streamUpdateReq.setTokenUsage(newTotal); + chatConversationService.updateChatConversationMy(streamUpdateReq, conversation.getUserId()); + } finally { + ContextUtil.setIgnore(false); + } + }).doOnError(throwable -> log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable)) + .onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR))); + } + + private String extractReasoningFromMetadata(ChatResponseMetadata metadata) { + if (metadata == null) return null; + String[] keys = new String[]{ + "reasoning_content", + "reasoning", + "thinking", + "deliberate_output", + "thoughts" + }; + for (String k : keys) { + Object v = metadata.get(k); + if (v != null) { + String s = String.valueOf(v); + if (StrUtil.isNotEmpty(s)) return s; } - - // 响应结果 - String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getText() : null; - newContent = StrUtil.nullToDefault(newContent, ""); - String answerChunk = stripReasoningTags(newContent); - contentBuffer.append(answerChunk); - - // 提取推理内容 - String newReasoningContent = null; - if (chunk.getMetadata() != null && chunk.getMetadata().containsKey("reasoning_content")) { - newReasoningContent = String.valueOf(chunk.getMetadata().get("reasoning_content")); - reasoningBuffer.append(newReasoningContent); - } else { - String extracted = extractReasoningFromText(newContent); - if (StrUtil.isNotEmpty(extracted)) { - newReasoningContent = extracted; - reasoningBuffer.append(extracted); - } - } - - return success(new AiChatMessageSendRespVO() - .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) - .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) - .setContent(answerChunk) - .setReasoningContent(newReasoningContent) - .setSegments(segments))); - }).doOnComplete(() -> { - // 手动设置租户信息(因为 Flux 异步会切换线程,导致 ThreadLocal 丢失) - try { - ContextUtil.setIgnore(true); - - String content = stripReasoningTags(contentBuffer.toString()); - String reasoningContent = reasoningBuffer.toString(); - AiChatMessageDO updateMessage = new AiChatMessageDO().setId(assistantMessage.getId()).setContent(content); - if (StrUtil.isNotEmpty(reasoningContent)) { - updateMessage.setReasoningContent(reasoningContent); - } - chatMessageMapper.updateById(updateMessage); - // 用量统计:prompt + completion(流式完成时) - int promptTokens = estimateContextTokens(conversation, historyMessages, sendReqVO.getContent()); - int completionTokens = estimateTokens(content); - int newTotal = (conversation.getTokenUsage() == null ? 0 : conversation.getTokenUsage()) + promptTokens + completionTokens; - AiChatConversationUpdateMyReqVO streamUpdateReq = new AiChatConversationUpdateMyReqVO(); - streamUpdateReq.setId(conversation.getId()); - streamUpdateReq.setTokenUsage(newTotal); - chatConversationService.updateChatConversationMy(streamUpdateReq, conversation.getUserId()); - } finally { - ContextUtil.setIgnore(false); - } - }).doOnError(throwable -> log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable)) - .onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR))); + } + return null; } private List recallKnowledgeSegment(String content, @@ -309,15 +513,6 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { if (StrUtil.isNotBlank(conversation.getSystemMessage())) { chatMessages.add(new SystemMessage(conversation.getSystemMessage())); } - // 1.1.1 深度思考系统提示(仅对支持的模型生效) - if (sendReqVO.getReasoningEnabled() && isReasoningSupported(model)) { - int thinkingBudget = 1024; - chatMessages.add(new SystemMessage( - "你是深度思考助手。先在‘思考过程’中进行详细推理,再给出最终答案。请保持推理与答案清晰分离。" + - "将推理内容使用 包裹,将最终答案使用 包裹。" + - StrUtil.format("思考阶段最多使用 {} token。", thinkingBudget) - )); - } // 1.2 历史 history message 历史消息 List contextMessages = filterContextMessages(messages, conversation, sendReqVO); @@ -347,15 +542,15 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { } // 2.2 构建 ChatOptions 对象 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); - ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(), + String effectiveModel = model.getModel(); + if (sendReqVO.getReasoningEnabled() && (platform == AiPlatformEnum.DEEP_SEEK || (platform == AiPlatformEnum.HUN_YUAN && StrUtil.startWithIgnoreCase(effectiveModel, "deepseek")))) { + effectiveModel = "deepseek-reasoner"; + } + ChatOptions chatOptions = AiUtils.buildChatOptions(platform, effectiveModel, conversation.getTemperature(), conversation.getMaxTokens(), toolNames, toolContext); return new Prompt(chatMessages, chatOptions); } - private boolean isReasoningSupported(AiModelDO model) { - return Boolean.TRUE.equals(model.getSupportsReasoning()); - } - private int estimateContextTokens(AiChatConversationDO conversation, List allMessages, String currentContent) { int total = 0; AiChatMessageSendReqVO ctxReq = new AiChatMessageSendReqVO(); diff --git a/luohuo-cloud/luohuo-oauth/luohuo-oauth-biz/src/main/java/com/luohuo/flex/oauth/granter/AbstractTokenGranter.java b/luohuo-cloud/luohuo-oauth/luohuo-oauth-biz/src/main/java/com/luohuo/flex/oauth/granter/AbstractTokenGranter.java index 7f1669c2..9cc816d4 100644 --- a/luohuo-cloud/luohuo-oauth/luohuo-oauth-biz/src/main/java/com/luohuo/flex/oauth/granter/AbstractTokenGranter.java +++ b/luohuo-cloud/luohuo-oauth/luohuo-oauth-biz/src/main/java/com/luohuo/flex/oauth/granter/AbstractTokenGranter.java @@ -445,6 +445,7 @@ public abstract class AbstractTokenGranter implements TokenGranter { } resultVO.setUid(uid); + obj.set(CLIENT_ID, clientId); resultVO.setRefreshToken(SaTempUtil.createToken(obj.toString(), 2 * saTokenConfig.getTimeout())); log.info("用户:{} 登录成功", userInfo.getUsername()); diff --git a/luohuo-cloud/luohuo-oauth/luohuo-oauth-biz/src/main/java/com/luohuo/flex/oauth/granter/RefreshTokenGranter.java b/luohuo-cloud/luohuo-oauth/luohuo-oauth-biz/src/main/java/com/luohuo/flex/oauth/granter/RefreshTokenGranter.java index b450839c..3ef38794 100644 --- a/luohuo-cloud/luohuo-oauth/luohuo-oauth-biz/src/main/java/com/luohuo/flex/oauth/granter/RefreshTokenGranter.java +++ b/luohuo-cloud/luohuo-oauth/luohuo-oauth-biz/src/main/java/com/luohuo/flex/oauth/granter/RefreshTokenGranter.java @@ -75,9 +75,10 @@ public class RefreshTokenGranter { Long tenantId = obj.getLong(HEADER_TENANT_ID); String systemType = obj.getStr(JWT_KEY_SYSTEM_TYPE); String device = obj.getStr(JWT_KEY_DEVICE); + String clientId = obj.getStr(CLIENT_ID); // 2、处理挤下线逻辑 - String combinedDeviceType = kickout(uid, userId, systemType, device); + String combinedDeviceType = kickout(uid, userId, systemType, device, clientId); // 3、为其生成新的短 token StpUtil.login(userId, new SaLoginModel().setDevice(combinedDeviceType)); @@ -85,6 +86,7 @@ public class RefreshTokenGranter { tokenSession.setLoginId(userId); tokenSession.set(JWT_KEY_SYSTEM_TYPE, systemType); tokenSession.set(JWT_KEY_DEVICE, device); + tokenSession.set(CLIENT_ID, clientId); if (topCompanyId != null) { tokenSession.set(JWT_KEY_TOP_COMPANY_ID, topCompanyId); @@ -129,7 +131,7 @@ public class RefreshTokenGranter { * @param deviceType 登录设备 * @return 组合后的设备类型 */ - private String kickout(Long uid, Long userId, String systemType, String deviceType) { + private String kickout(Long uid, Long userId, String systemType, String deviceType, String currentClientId) { // 1. 组合完整的设备类型标识 String combinedDeviceType = ToolsUtil.combineStrings(systemType, deviceType); @@ -155,10 +157,13 @@ public class RefreshTokenGranter { for (String token : sameDeviceTokens) { try { String clientId = StpUtil.getTokenSessionByToken(token).getString(CLIENT_ID); - StpUtil.kickout(token); - log.info("刷新token时已踢出旧会话: token={}", token); - - SpringUtils.publishEvent(new TokenExpireEvent(this, new OffLineResp(uid, deviceType, clientId, ContextUtil.getIP(), token))); + if (currentClientId == null || !currentClientId.equals(clientId)) { + StpUtil.kickout(token); + log.info("刷新token时已踢出旧会话: token={}", token); + SpringUtils.publishEvent(new TokenExpireEvent(this, new OffLineResp(uid, deviceType, clientId, ContextUtil.getIP(), token))); + } else { + log.info("刷新token时跳过当前客户端会话: token={}, clientId={}", token, clientId); + } } catch (Exception e) { log.error("刷新token时踢出旧会话失败: token={}", token, e); } diff --git a/luohuo-cloud/luohuo-public/luohuo-config-sdk/src/main/java/com/luohuo/flex/entity/IceServer.java b/luohuo-cloud/luohuo-public/luohuo-config-sdk/src/main/java/com/luohuo/flex/entity/IceServer.java new file mode 100644 index 00000000..07927815 --- /dev/null +++ b/luohuo-cloud/luohuo-public/luohuo-config-sdk/src/main/java/com/luohuo/flex/entity/IceServer.java @@ -0,0 +1,13 @@ +package com.luohuo.flex.entity; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +@Data +public class IceServer implements Serializable { + private List urls; + private String username; + private String credential; +} diff --git a/luohuo-cloud/luohuo-public/luohuo-config-sdk/src/main/java/com/luohuo/flex/entity/Init.java b/luohuo-cloud/luohuo-public/luohuo-config-sdk/src/main/java/com/luohuo/flex/entity/Init.java index 5246dbe8..fc00a0d9 100644 --- a/luohuo-cloud/luohuo-public/luohuo-config-sdk/src/main/java/com/luohuo/flex/entity/Init.java +++ b/luohuo-cloud/luohuo-public/luohuo-config-sdk/src/main/java/com/luohuo/flex/entity/Init.java @@ -15,4 +15,6 @@ public class Init implements Serializable { private String roomGroupId; // 七牛云配置 private QiNiu qiNiu; + // ICE Server 配置 + private IceServer iceServer; } diff --git a/luohuo-cloud/luohuo-public/luohuo-config-sdk/src/main/java/com/luohuo/flex/service/SysConfigService.java b/luohuo-cloud/luohuo-public/luohuo-config-sdk/src/main/java/com/luohuo/flex/service/SysConfigService.java index c7d1e51a..addda450 100644 --- a/luohuo-cloud/luohuo-public/luohuo-config-sdk/src/main/java/com/luohuo/flex/service/SysConfigService.java +++ b/luohuo-cloud/luohuo-public/luohuo-config-sdk/src/main/java/com/luohuo/flex/service/SysConfigService.java @@ -43,6 +43,4 @@ public interface SysConfigService { * 获取系统全局配置 */ Init getSystemInit(); - - T getBeanByName(String name, Class t); } diff --git a/luohuo-cloud/luohuo-public/luohuo-config-sdk/src/main/java/com/luohuo/flex/service/impl/SysSysConfigServiceImpl.java b/luohuo-cloud/luohuo-public/luohuo-config-sdk/src/main/java/com/luohuo/flex/service/impl/SysSysConfigServiceImpl.java index 2c420a40..646e829d 100644 --- a/luohuo-cloud/luohuo-public/luohuo-config-sdk/src/main/java/com/luohuo/flex/service/impl/SysSysConfigServiceImpl.java +++ b/luohuo-cloud/luohuo-public/luohuo-config-sdk/src/main/java/com/luohuo/flex/service/impl/SysSysConfigServiceImpl.java @@ -1,27 +1,24 @@ package com.luohuo.flex.service.impl; -import cn.hutool.core.convert.Convert; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.luohuo.flex.entity.IceServer; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import com.luohuo.basic.cache.redis2.CacheResult; import com.luohuo.basic.cache.repository.CachePlusOps; -import com.luohuo.basic.exception.BizException; -import com.luohuo.basic.model.cache.CacheHashKey; import com.luohuo.flex.common.cache.common.ConfigCacheKeyBuilder; import com.luohuo.flex.entity.Config; import com.luohuo.flex.entity.Init; import com.luohuo.flex.entity.QiNiu; import com.luohuo.flex.mapper.SysConfigMapper; import com.luohuo.flex.service.SysConfigService; -import com.luohuo.flex.vo.config.ConfigParam; +import java.util.Arrays; import java.util.List; -import java.util.function.Function; /** * 参数配置 服务层实现 @@ -62,10 +59,6 @@ public class SysSysConfigServiceImpl implements SysConfigService { init(); } - public T indexInit(String key, Function loader) { - return cachePlusOps.hGet(ConfigCacheKeyBuilder.build(key), loader).getValue(); - } - /** * 初始化配置 */ @@ -75,27 +68,6 @@ public class SysSysConfigServiceImpl implements SysConfigService { getSystemInit(); } - /** - * 把数据同步到redis - */ - private void deleteRedis(String name) { - cachePlusOps.hDel(ConfigCacheKeyBuilder.build(name)); - } - - /** - * 缓存转Bean对象 - * @param name - * @param 返回的类型 - * @return - */ - public T getBeanByName(String name, Class t) { - String data = get(name); - if (StrUtil.isEmpty(data)) { - return null; - } - return JSON.parseObject(data, t); - } - /** * 走缓存获取键值 * @param name @@ -119,66 +91,35 @@ public class SysSysConfigServiceImpl implements SysConfigService { } } - public Long getLong(String name) { - return Long.parseLong(get(name)); - } + public Init getSystemInit() { + Init init = new Init(); + init.setLogo(get("logo")); + init.setName(get("systemName")); + init.setRoomGroupId(get("roomGroupId")); - public Integer getInteger(String name) { - return Integer.parseInt(get(name)); - } + QiNiu qiNiu = new QiNiu(); + qiNiu.setOssDomain(get("qnStorageCDN")); + qiNiu.setFragmentSize(get("fragmentSize")); + qiNiu.setTurnSharSize(get("turnSharSize")); + init.setQiNiu(qiNiu); - /** - * 根据 name 获取 value 找不到抛异常 - * @param name menu name - * @return String - */ - public String getValueByKeyException(String name) { - String value = get(name); - if (null == value) { - throw new BizException("没有找到"+ name +"数据"); - } + // ICE Server + var ice = new IceServer(); + String urlsRaw = get("iceServerUrls"); + java.util.List urls; + try { + urls = JSON.parseArray(urlsRaw, String.class); + } catch (Exception _e) { + urls = Arrays.stream((urlsRaw == null ? "" : urlsRaw).split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .toList(); + } + ice.setUrls(urls); + ice.setUsername(get("iceServerUsername")); + ice.setCredential(get("iceServerCredential")); + init.setIceServer(ice); - return value; - } - - /** - * 获取验证码开关 - * - * @return true开启,false关闭 - */ - public boolean selectCaptchaEnabled() { - String captchaEnabled = get("captchaEnabled"); - if (StrUtil.isEmpty(captchaEnabled)) { - return true; - } - return Convert.toBool(captchaEnabled); - } - - public Init getSystemInit() { - Init init = new Init(); - init.setLogo(get("logo")); - init.setName(get("systemName")); - init.setRoomGroupId(get("roomGroupId")); - - QiNiu qiNiu = new QiNiu(); - qiNiu.setOssDomain(get("qnStorageCDN")); - qiNiu.setFragmentSize(get("fragmentSize")); - qiNiu.setTurnSharSize(get("turnSharSize")); - init.setQiNiu(qiNiu); - return init; - } - - public List selectConfigList(ConfigParam params) { - return configMapper.selectList(new QueryWrapper() - .eq("type", params.getType()) - .like("config_name", params.getConfigName()) - .like("config_key", params.getConfigKey())); - } - - public Config selectConfigByConfigName(Config config) { - return configMapper.selectOne(new QueryWrapper<>(Config.class) - .eq("config_key", config.getConfigKey()) - .eq("type", config.getType()) - .last(" limit 1")); - } + return init; + } } diff --git a/luohuo-cloud/luohuo-system/luohuo-system-biz/src/main/java/com/luohuo/flex/flex/storage/StorageDriver.java b/luohuo-cloud/luohuo-system/luohuo-system-biz/src/main/java/com/luohuo/flex/flex/storage/StorageDriver.java index fea5eba7..2ff3566e 100644 --- a/luohuo-cloud/luohuo-system/luohuo-system-biz/src/main/java/com/luohuo/flex/flex/storage/StorageDriver.java +++ b/luohuo-cloud/luohuo-system/luohuo-system-biz/src/main/java/com/luohuo/flex/flex/storage/StorageDriver.java @@ -2,7 +2,7 @@ package com.luohuo.flex.flex.storage; import com.alibaba.fastjson.JSONObject; import com.luohuo.flex.flex.storage.engine.QiNiuStorage; -import jakarta.annotation.PostConstruct; +import com.luohuo.flex.flex.storage.engine.MinioStorage; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import com.luohuo.flex.service.SysConfigService; @@ -14,55 +14,42 @@ import java.util.Map; @RequiredArgsConstructor public class StorageDriver { - private final SysConfigService sysConfigService; + private final SysConfigService sysConfigService; - /** - * 当前存储引擎 - */ - private String engine; + private String currentEngine() { + String e = sysConfigService.get("storageDefault"); + return e == null || e.isBlank() ? "qiniu" : e; + } - /** - * 存储引擎配置 - */ - private Map config = new HashMap<>(); + public Map getConfig() { + Map cfg = new HashMap<>(); + cfg.put("storagePrefix", sysConfigService.get("qnStorageName")); + cfg.put("qnUploadUrl", sysConfigService.get("qnUploadUrl")); + cfg.put("qnAccessKey", sysConfigService.get("qnAccessKey")); + cfg.put("qnSecretKey", sysConfigService.get("qnSecretKey")); + cfg.put("qnStorageName", sysConfigService.get("qnStorageName")); + cfg.put("minioEndpoint", sysConfigService.get("minioEndpoint")); + cfg.put("minioAccessKey", sysConfigService.get("minioAccessKey")); + cfg.put("minioSecretKey", sysConfigService.get("minioSecretKey")); + cfg.put("minioBucket", sysConfigService.get("minioBucket")); + cfg.put("minioUrlPrefix", sysConfigService.get("minioUrlPrefix")); + return cfg; + } - @PostConstruct - public void init() { - this.engine = sysConfigService.get("storageDefault"); - config.put("storagePrefix", sysConfigService.get("qnStorageName")); - config.put("qnUploadUrl", sysConfigService.get("qnUploadUrl")); - config.put("qnAccessKey", sysConfigService.get("qnAccessKey")); - config.put("qnSecretKey", sysConfigService.get("qnSecretKey")); - config.put("qnStorageName", sysConfigService.get("qnStorageName")); - } - - public Map getConfig() { - return config; - } - - - public JSONObject getToken() { - switch (this.engine) { - case "qiniu" -> { - return new QiNiuStorage(this.getConfig()).upToken(); - } - } - return null; - } - - /** - * 上传字节数组到存储服务 - * - * @param data 文件字节数组 - * @param fileExtension 文件扩展名(如 "mp3") - * @return 文件访问URL - */ - public String uploadBytes(byte[] data, String fileExtension) { - switch (this.engine) { - case "qiniu" -> { - return new QiNiuStorage(this.getConfig()).uploadBytes(data, fileExtension); - } - default -> throw new IllegalArgumentException("不支持的存储引擎: " + this.engine); - } - } + public JSONObject getToken(String scene, String fileName) { + String engine = currentEngine(); + Map cfg = getConfig(); + switch (engine) { + case "qiniu" -> { + return new QiNiuStorage(cfg).upToken(); + } + case "minio" -> { + String safeScene = scene == null || scene.isBlank() ? "chat" : scene; + String safeName = fileName == null || fileName.isBlank() ? "file" : fileName; + String objectKey = safeScene + "/" + System.currentTimeMillis() + "_" + safeName; + return new MinioStorage(cfg).presignPut(objectKey); + } + default -> throw new IllegalArgumentException("不支持的存储引擎: " + engine); + } + } } diff --git a/luohuo-cloud/luohuo-system/luohuo-system-biz/src/main/java/com/luohuo/flex/flex/storage/engine/MinioStorage.java b/luohuo-cloud/luohuo-system/luohuo-system-biz/src/main/java/com/luohuo/flex/flex/storage/engine/MinioStorage.java new file mode 100644 index 00000000..91ecd965 --- /dev/null +++ b/luohuo-cloud/luohuo-system/luohuo-system-biz/src/main/java/com/luohuo/flex/flex/storage/engine/MinioStorage.java @@ -0,0 +1,60 @@ +package com.luohuo.flex.flex.storage.engine; + +import com.alibaba.fastjson.JSONObject; +import io.minio.GetPresignedObjectUrlArgs; +import io.minio.MinioClient; +import io.minio.http.Method; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +@Slf4j +public class MinioStorage { + + private final Map config; + + public MinioStorage(Map config) { + this.config = config; + } + + public JSONObject presignPut(String objectKey) { + String endpoint = this.config.getOrDefault("minioEndpoint", ""); + String accessKey = this.config.getOrDefault("minioAccessKey", ""); + String secretKey = this.config.getOrDefault("minioSecretKey", ""); + String bucket = this.config.getOrDefault("minioBucket", ""); + String urlPrefix = this.config.getOrDefault("minioUrlPrefix", ""); + + try { + MinioClient client = MinioClient.builder() + .endpoint(endpoint) + .credentials(accessKey, secretKey) + .build(); + + String uploadUrl = client.getPresignedObjectUrl( + GetPresignedObjectUrlArgs.builder() + .method(Method.PUT) + .bucket(bucket) + .object(objectKey) + .expiry(60 * 60) + .build() + ); + + String downloadUrl; + if (urlPrefix != null && !urlPrefix.isBlank()) { + downloadUrl = urlPrefix.endsWith("/") ? (urlPrefix + objectKey) : (urlPrefix + "/" + objectKey); + } else { + String base = endpoint.endsWith("/") ? endpoint : (endpoint + "/"); + downloadUrl = base + bucket + "/" + objectKey; + } + + JSONObject json = new JSONObject(); + json.put("uploadUrl", uploadUrl); + json.put("downloadUrl", downloadUrl); + json.put("objectKey", objectKey); + return json; + } catch (Exception e) { + log.error("[Minio presignPut] 生成预签名失败, objectKey:{}", objectKey, e); + throw new RuntimeException("MinIO 预签名失败: " + e.getMessage(), e); + } + } +} diff --git a/luohuo-cloud/luohuo-system/luohuo-system-biz/src/main/java/com/luohuo/flex/service/TranslateService.java b/luohuo-cloud/luohuo-system/luohuo-system-biz/src/main/java/com/luohuo/flex/service/TranslateService.java new file mode 100644 index 00000000..b0b29715 --- /dev/null +++ b/luohuo-cloud/luohuo-system/luohuo-system-biz/src/main/java/com/luohuo/flex/service/TranslateService.java @@ -0,0 +1,10 @@ +package com.luohuo.flex.service; + +import java.util.List; + +public interface TranslateService { + + String translate(String text, String provider, String source, String target); + + List translateSegments(String text, String provider, String source, String target); +} diff --git a/luohuo-cloud/luohuo-system/luohuo-system-biz/src/main/java/com/luohuo/flex/service/impl/TranslateServiceImpl.java b/luohuo-cloud/luohuo-system/luohuo-system-biz/src/main/java/com/luohuo/flex/service/impl/TranslateServiceImpl.java new file mode 100644 index 00000000..e47e1bd8 --- /dev/null +++ b/luohuo-cloud/luohuo-system/luohuo-system-biz/src/main/java/com/luohuo/flex/service/impl/TranslateServiceImpl.java @@ -0,0 +1,229 @@ +package com.luohuo.flex.service.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.luohuo.flex.service.SysConfigService; +import com.luohuo.flex.service.TranslateService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +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.client.RestTemplate; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class TranslateServiceImpl implements TranslateService { + + private final RestTemplate restTemplate; + private final SysConfigService sysConfigService; + + @Override + public String translate(String text, String provider, String source, String target) { + if (text == null || text.trim().isEmpty()) { + return ""; + } + String p = provider; + if (p == null || p.isBlank()) { + String conf = sysConfigService.get("translateDefault"); + p = (conf == null || conf.isBlank()) ? "tencent" : conf.toLowerCase(); + } else { + p = p.toLowerCase(); + } + return switch (p) { + case "youdao" -> youdaoTranslate(text, source == null ? "auto" : source, target == null ? "zh-CHS" : target); + case "tencent" -> tencentTranslate(text, source == null ? "auto" : source, target == null ? "zh" : target); + default -> youdaoTranslate(text, source == null ? "auto" : source, target == null ? "zh-CHS" : target); + }; + } + + @Override + public List translateSegments(String text, String provider, String source, String target) { + String out = translate(text, provider, source, target); + return splitSegments(out); + } + + private List splitSegments(String text) { + List result = new ArrayList<>(); + if (text == null || text.isEmpty()) { + return result; + } + String[] parts = text.split("(?<=[。!?.!?;;\n])"); + for (String p : parts) { + String s = p.trim(); + if (!s.isEmpty()) { + result.add(s); + } + } + if (result.isEmpty()) { + result.add(text); + } + return result; + } + + private String youdaoTranslate(String text, String source, String target) { + String appKey = sysConfigService.get("youdaoAppKey"); + String appSecret = sysConfigService.get("youdaoAppSecret"); + if (isBlank(appKey) || isBlank(appSecret)) { + throw new IllegalStateException("有道翻译密钥未配置"); + } + + long salt = System.currentTimeMillis(); + long curtime = System.currentTimeMillis() / 1000; + String sign = youdaoSign(text, appKey, salt, curtime, appSecret); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("q", text); + body.add("from", source); + body.add("to", target); + body.add("appKey", appKey); + body.add("salt", String.valueOf(salt)); + body.add("sign", sign); + body.add("signType", "v3"); + body.add("curtime", String.valueOf(curtime)); + + HttpEntity> entity = new HttpEntity<>(body, headers); + String resp = restTemplate.postForObject("https://openapi.youdao.com/api", entity, String.class); + if (resp == null) { + throw new IllegalStateException("有道翻译响应为空"); + } + JSONObject json = JSON.parseObject(resp); + String errorCode = json.getString("errorCode"); + if (!"0".equals(errorCode)) { + throw new IllegalStateException("有道翻译失败:" + errorCode); + } + var arr = json.getJSONArray("translation"); + if (arr == null || arr.isEmpty()) { + throw new IllegalStateException("有道翻译结果为空"); + } + return arr.getString(0); + } + + private String tencentTranslate(String text, String source, String target) { + String secretId = sysConfigService.get("tencentSecretId"); + String secretKey = sysConfigService.get("tencentApiKey"); + if (isBlank(secretId) || isBlank(secretKey)) { + throw new IllegalStateException("腾讯云翻译密钥未配置"); + } + + long timestamp = Instant.now().getEpochSecond(); + String dateStr = DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneOffset.UTC).format(Instant.ofEpochSecond(timestamp)); + String service = "tmt"; + + JSONObject payload = new JSONObject(); + payload.put("SourceText", text); + payload.put("Source", source); + payload.put("Target", target); + payload.put("ProjectId", 0); + + String hashedRequestPayload = sha256Hex(payload.toJSONString()); + String canonicalRequest = String.join("\n", + "POST", + "/", + "", + "content-type:application/json", + "host:tmt.tencentcloudapi.com", + "", + "content-type;host", + hashedRequestPayload + ); + String credentialScope = dateStr + "/" + service + "/tc3_request"; + String hashedCanonicalRequest = sha256Hex(canonicalRequest); + String stringToSign = String.join("\n", + "TC3-HMAC-SHA256", + String.valueOf(timestamp), + credentialScope, + hashedCanonicalRequest + ); + + byte[] kDate = hmacSha256(dateStr.getBytes(StandardCharsets.UTF_8), ("TC3" + secretKey).getBytes(StandardCharsets.UTF_8)); + byte[] kService = hmacSha256(service.getBytes(StandardCharsets.UTF_8), kDate); + byte[] kSigning = hmacSha256("tc3_request".getBytes(StandardCharsets.UTF_8), kService); + String signature = bytesToHex(hmacSha256(stringToSign.getBytes(StandardCharsets.UTF_8), kSigning)); + + String authorization = "TC3-HMAC-SHA256 Credential=" + secretId + "/" + credentialScope + ", SignedHeaders=content-type;host, Signature=" + signature; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.add("X-TC-Action", "TextTranslate"); + headers.add("X-TC-Version", "2018-03-21"); + headers.add("X-TC-Region", "ap-guangzhou"); + headers.add("X-TC-Timestamp", String.valueOf(timestamp)); + headers.add("X-TC-SecretId", secretId); + headers.add("Authorization", authorization); + + HttpEntity entity = new HttpEntity<>(payload.toJSONString(), headers); + String resp = restTemplate.postForObject("https://tmt.tencentcloudapi.com", entity, String.class); + if (resp == null) { + throw new IllegalStateException("腾讯云翻译响应为空"); + } + JSONObject json = JSON.parseObject(resp); + JSONObject response = json.getJSONObject("Response"); + if (response == null) { + throw new IllegalStateException("腾讯云翻译结果为空"); + } + if (response.containsKey("Error")) { + String msg = response.getJSONObject("Error").getString("Message"); + throw new IllegalStateException("腾讯云翻译失败:" + msg); + } + String targetText = response.getString("TargetText"); + if (isBlank(targetText)) { + throw new IllegalStateException("腾讯云翻译结果为空"); + } + return targetText; + } + + private String youdaoSign(String text, String appKey, long salt, long curtime, String appSecret) { + String input = text.length() <= 20 ? text : text.substring(0, 10) + text.length() + text.substring(text.length() - 10); + String raw = appKey + input + salt + curtime + appSecret; + return sha256Hex(raw); + } + + private String sha256Hex(String s) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(s.getBytes(StandardCharsets.UTF_8)); + return bytesToHex(digest); + } catch (Exception e) { + throw new IllegalStateException("SHA-256 计算失败", e); + } + } + + private byte[] hmacSha256(byte[] data, byte[] key) { + try { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(key, "HmacSHA256")); + return mac.doFinal(data); + } catch (Exception e) { + throw new IllegalStateException("HMAC-SHA256 计算失败", e); + } + } + + private String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + String h = Integer.toHexString(b & 0xff); + if (h.length() == 1) sb.append('0'); + sb.append(h); + } + return sb.toString(); + } + + private boolean isBlank(String s) { + return s == null || s.trim().isEmpty(); + } +} diff --git a/luohuo-cloud/luohuo-system/luohuo-system-controller/src/main/java/com/luohuo/flex/IndexController.java b/luohuo-cloud/luohuo-system/luohuo-system-controller/src/main/java/com/luohuo/flex/IndexController.java index 5f100544..65be1184 100644 --- a/luohuo-cloud/luohuo-system/luohuo-system-controller/src/main/java/com/luohuo/flex/IndexController.java +++ b/luohuo-cloud/luohuo-system/luohuo-system-controller/src/main/java/com/luohuo/flex/IndexController.java @@ -39,10 +39,19 @@ public class IndexController { return R.success(sysConfigService.getSystemInit()); } - @Operation(summary = "获取七牛云上传token") - @GetMapping("/ossToken") - public R token() { - return R.success(storageDriver.getToken()); + @Operation(summary = "获取统一直传凭证(根据引擎返回 七牛token 或 MinIO预签名)") + @GetMapping("/ossToken") + public R token(@RequestParam(required = false, defaultValue = "chat") String scene, @RequestParam(required = false) String fileName) { + return R.success(storageDriver.getToken(scene, fileName)); + } + + @GetMapping("/storage/provider") + @Operation(summary = "获取默认存储提供者") + public R storageProvider() { + var provider = sysConfigService.get("storageDefault"); + var json = new JSONObject(); + json.put("provider", provider); + return R.success(json); } @GetMapping("/config/list") diff --git a/luohuo-cloud/luohuo-system/luohuo-system-controller/src/main/java/com/luohuo/flex/TranslateController.java b/luohuo-cloud/luohuo-system/luohuo-system-controller/src/main/java/com/luohuo/flex/TranslateController.java new file mode 100644 index 00000000..f04da78b --- /dev/null +++ b/luohuo-cloud/luohuo-system/luohuo-system-controller/src/main/java/com/luohuo/flex/TranslateController.java @@ -0,0 +1,85 @@ +package com.luohuo.flex; + +import com.luohuo.basic.base.R; +import com.luohuo.flex.dto.TranslateRequest; +import com.luohuo.flex.dto.TranslateResponse; +import com.luohuo.flex.dto.TranslateSegmentsResponse; +import com.luohuo.flex.service.TranslateService; +import com.luohuo.flex.service.SysConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.time.Duration; +import java.util.List; + +@Slf4j +@RestController +@RequestMapping("/anyTenant/translate") +@RequiredArgsConstructor +@Tag(name = "翻译服务") +public class TranslateController { + + private final TranslateService translateService; + private final SysConfigService sysConfigService; + + @PostMapping("/text") + @Operation(summary = "普通翻译(一次性返回)") + public R translate(@RequestBody TranslateRequest req) { + String source = req.getSource() == null ? "auto" : req.getSource(); + String provider = resolveProvider(req.getProvider()); + String result = translateService.translate(req.getText(), provider, source, req.getTarget()); + String usedProvider = provider; + return R.success(new TranslateResponse(result, usedProvider)); + } + + @PostMapping("/segment") + @Operation(summary = "段式翻译") + public R translateSegments(@RequestBody TranslateRequest req) { + String source = req.getSource() == null ? "auto" : req.getSource(); + String provider = resolveProvider(req.getProvider()); + List segments = translateService.translateSegments(req.getText(), provider, source, req.getTarget()); + String usedProvider = provider; + return R.success(new TranslateSegmentsResponse(segments, usedProvider)); + } + + @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + @Operation(summary = "流式翻译") + public SseEmitter translateStream(@RequestParam String text, + @RequestParam(required = false) String provider, + @RequestParam(required = false, defaultValue = "auto") String source, + @RequestParam(required = false) String target) { + SseEmitter emitter = new SseEmitter(0L); + try { + String resolved = resolveProvider(provider); + List segments = translateService.translateSegments(text, resolved, source, target); + for (String seg : segments) { + emitter.send(SseEmitter.event().name("segment").data(seg)); + try { Thread.sleep(Duration.ofMillis(120).toMillis()); } catch (InterruptedException ignored) {} + } + emitter.send(SseEmitter.event().name("end").data("done")); + emitter.complete(); + } catch (IOException e) { + emitter.completeWithError(e); + } catch (Exception ex) { + try { + emitter.send(SseEmitter.event().name("error").data(ex.getMessage())); + } catch (IOException ignored) {} + emitter.completeWithError(ex); + } + return emitter; + } + + private String resolveProvider(String provider) { + if (provider != null && !provider.isBlank()) { + return provider.toLowerCase(); + } + String conf = sysConfigService.get("translateDefault"); + return (conf == null || conf.isBlank()) ? "tencent" : conf.toLowerCase(); + } +} diff --git a/luohuo-cloud/luohuo-system/luohuo-system-controller/src/main/java/com/luohuo/flex/dto/TranslateRequest.java b/luohuo-cloud/luohuo-system/luohuo-system-controller/src/main/java/com/luohuo/flex/dto/TranslateRequest.java new file mode 100644 index 00000000..58ed1adb --- /dev/null +++ b/luohuo-cloud/luohuo-system/luohuo-system-controller/src/main/java/com/luohuo/flex/dto/TranslateRequest.java @@ -0,0 +1,11 @@ +package com.luohuo.flex.dto; + +import lombok.Data; + +@Data +public class TranslateRequest { + private String text; + private String provider; // youdao / tencent,可选 + private String source; // 源语言,默认 auto + private String target; // 目标语言,默认 zh 或 zh-CHS +} diff --git a/luohuo-cloud/luohuo-system/luohuo-system-controller/src/main/java/com/luohuo/flex/dto/TranslateResponse.java b/luohuo-cloud/luohuo-system/luohuo-system-controller/src/main/java/com/luohuo/flex/dto/TranslateResponse.java new file mode 100644 index 00000000..46a2fc92 --- /dev/null +++ b/luohuo-cloud/luohuo-system/luohuo-system-controller/src/main/java/com/luohuo/flex/dto/TranslateResponse.java @@ -0,0 +1,11 @@ +package com.luohuo.flex.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class TranslateResponse { + private String text; + private String provider; +} diff --git a/luohuo-cloud/luohuo-system/luohuo-system-controller/src/main/java/com/luohuo/flex/dto/TranslateSegmentsResponse.java b/luohuo-cloud/luohuo-system/luohuo-system-controller/src/main/java/com/luohuo/flex/dto/TranslateSegmentsResponse.java new file mode 100644 index 00000000..166c0a38 --- /dev/null +++ b/luohuo-cloud/luohuo-system/luohuo-system-controller/src/main/java/com/luohuo/flex/dto/TranslateSegmentsResponse.java @@ -0,0 +1,13 @@ +package com.luohuo.flex.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +@Data +@AllArgsConstructor +public class TranslateSegmentsResponse { + private List segments; + private String provider; +} diff --git a/luohuo-cloud/pom.xml b/luohuo-cloud/pom.xml index 169204e8..e9d95c89 100644 --- a/luohuo-cloud/pom.xml +++ b/luohuo-cloud/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.luohuo.flex luohuo-cloud - 3.0.5 + 3.0.6 pom https://gitee.com/HuLaSpark/HuLa-Server ${project.artifactId} diff --git a/luohuo-util/pom.xml b/luohuo-util/pom.xml index 7e8d16ad..30820549 100644 --- a/luohuo-util/pom.xml +++ b/luohuo-util/pom.xml @@ -6,12 +6,12 @@ com.luohuo.basic luohuo-parent - 3.0.5 + 3.0.6 luohuo-parent/pom.xml luohuo-util - 3.0.5 + 3.0.6 pom 4.0.0