feat: 新增minio存储、对接各个平台深度思考模型

This commit is contained in:
乾乾
2025-12-05 13:36:18 +08:00
parent 8cdc3cf98c
commit 90241d7a6c
31 changed files with 1350 additions and 272 deletions
+25
View File
@@ -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
+26 -12
View File
@@ -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
+33 -19
View File
@@ -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
@@ -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<Chunk> streamChat(String model,
List<Map<String, String>> messages,
Integer maxTokens,
Double temperature) {
return Flux.create(sink -> {
try {
Map<String, Object> 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);
}
}
@@ -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<Map<String, String>> messages,
Integer maxTokens,
Double temperature) throws Exception {
Map<String, Object> 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<String> 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;
}
}
@@ -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<Chunk> streamChat(String model,
List<Map<String, String>> messages,
Integer maxTokens,
Double temperature) {
return Flux.create(sink -> {
try {
Map<String, Object> 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);
}
}
@@ -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<Map<String, String>> messages, Integer maxTokens, Double temperature) throws Exception;
}
@@ -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;
}
}
@@ -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<ReasoningChunk> stream(String baseUrl, String apiKey, String model, List<Map<String, String>> messages, Integer maxTokens, Double temperature);
}
@@ -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<ReasoningChunk> stream(String baseUrl, String apiKey, String model, List<Map<String, String>> 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;
});
}
}
@@ -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<Map<String, String>> 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;
}
}
@@ -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<ReasoningChunk> stream(String baseUrl, String apiKey, String model, List<Map<String, String>> 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;
});
}
}
@@ -0,0 +1,6 @@
package com.luohuo.flex.ai.core.model.strategy.chat;
public class ReasoningChunk {
public String content;
public String reasoning;
}
@@ -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<Message> instructions = prompt.getInstructions();
List<Map<String, String>> 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<String, String> 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<Long, AiKnowledgeDocumentDO> 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<Message> instructions = prompt.getInstructions();
List<Map<String, String>> 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<String, String> item = Map.of("role", role, "content", m.getText());
messages.add(item);
}
Flux<ReasoningChunk> 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<AiChatMessageRespVO.KnowledgeSegment> segments = null;
if (StrUtil.isEmpty(contentBuffer)) {
Map<Long, AiKnowledgeDocumentDO> 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<ChatResponse> streamResponse = chatModel.stream(prompt);
StringBuffer fbContentBuffer = new StringBuffer();
StringBuffer fbReasoningBuffer = new StringBuffer();
return streamResponse.map(chunk -> {
List<AiChatMessageRespVO.KnowledgeSegment> segments = null;
if (StrUtil.isEmpty(fbContentBuffer)) {
Map<Long, AiKnowledgeDocumentDO> 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<ChatResponse> streamResponse = chatModel.stream(prompt);
// 4.3 流式返回
StringBuffer contentBuffer = new StringBuffer();
StringBuffer reasoningBuffer = new StringBuffer(); // 用于累积推理内容
StringBuffer reasoningBuffer = new StringBuffer();
return streamResponse.map(chunk -> {
// 处理知识库的返回,只有首次才有
List<AiChatMessageRespVO.KnowledgeSegment> segments = null;
if (StrUtil.isEmpty(contentBuffer)) {
Map<Long, AiKnowledgeDocumentDO> 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<AiChatMessageRespVO.KnowledgeSegment> segments = null;
if (StrUtil.isEmpty(contentBuffer)) {
Map<Long, AiKnowledgeDocumentDO> 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<AiKnowledgeSegmentSearchRespBO> 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(
"你是深度思考助手。先在‘思考过程’中进行详细推理,再给出最终答案。请保持推理与答案清晰分离。" +
"将推理内容使用 <thinking> 与 </thinking> 包裹,将最终答案使用 <answer> 与 </answer> 包裹。" +
StrUtil.format("思考阶段最多使用 {} token。", thinkingBudget)
));
}
// 1.2 历史 history message 历史消息
List<AiChatMessageDO> 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<AiChatMessageDO> allMessages, String currentContent) {
int total = 0;
AiChatMessageSendReqVO ctxReq = new AiChatMessageSendReqVO();
@@ -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());
@@ -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);
}
@@ -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<String> urls;
private String username;
private String credential;
}
@@ -15,4 +15,6 @@ public class Init implements Serializable {
private String roomGroupId;
// 七牛云配置
private QiNiu qiNiu;
// ICE Server 配置
private IceServer iceServer;
}
@@ -43,6 +43,4 @@ public interface SysConfigService {
* 获取系统全局配置
*/
Init getSystemInit();
<T> T getBeanByName(String name, Class<T> t);
}
@@ -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> T indexInit(String key, Function<CacheHashKey, ? extends T> 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 <T> 返回的类型
* @return
*/
public <T> T getBeanByName(String name, Class<T> 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<String> 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<Config> selectConfigList(ConfigParam params) {
return configMapper.selectList(new QueryWrapper<Config>()
.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;
}
}
@@ -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<String, String> config = new HashMap<>();
public Map<String, String> getConfig() {
Map<String, String> 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<String, String> 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<String, String> 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);
}
}
}
@@ -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<String, String> config;
public MinioStorage(Map<String, String> 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);
}
}
}
@@ -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<String> translateSegments(String text, String provider, String source, String target);
}
@@ -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<String> translateSegments(String text, String provider, String source, String target) {
String out = translate(text, provider, source, target);
return splitSegments(out);
}
private List<String> splitSegments(String text) {
List<String> 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<String, String> 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<MultiValueMap<String, String>> 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<String> 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();
}
}
@@ -39,10 +39,19 @@ public class IndexController {
return R.success(sysConfigService.getSystemInit());
}
@Operation(summary = "获取七牛云上传token")
@GetMapping("/ossToken")
public R<JSONObject> token() {
return R.success(storageDriver.getToken());
@Operation(summary = "获取统一直传凭证(根据引擎返回 七牛token 或 MinIO预签名)")
@GetMapping("/ossToken")
public R<JSONObject> 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<JSONObject> storageProvider() {
var provider = sysConfigService.get("storageDefault");
var json = new JSONObject();
json.put("provider", provider);
return R.success(json);
}
@GetMapping("/config/list")
@@ -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<TranslateResponse> 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<TranslateSegmentsResponse> translateSegments(@RequestBody TranslateRequest req) {
String source = req.getSource() == null ? "auto" : req.getSource();
String provider = resolveProvider(req.getProvider());
List<String> 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<String> 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();
}
}
@@ -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
}
@@ -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;
}
@@ -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<String> segments;
private String provider;
}
+1 -1
View File
@@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.luohuo.flex</groupId>
<artifactId>luohuo-cloud</artifactId>
<version>3.0.5</version>
<version>3.0.6</version>
<packaging>pom</packaging>
<url>https://gitee.com/HuLaSpark/HuLa-Server</url>
<name>${project.artifactId}</name>
+2 -2
View File
@@ -6,12 +6,12 @@
<parent>
<groupId>com.luohuo.basic</groupId>
<artifactId>luohuo-parent</artifactId>
<version>3.0.5</version>
<version>3.0.6</version>
<relativePath>luohuo-parent/pom.xml</relativePath>
</parent>
<artifactId>luohuo-util</artifactId>
<version>3.0.5</version>
<version>3.0.6</version>
<packaging>pom</packaging>
<modelVersion>4.0.0</modelVersion>