mirror of
https://github.com/HuLaSpark/HuLa-Server.git
synced 2026-08-28 15:52:58 +08:00
feat: 新增minio存储、对接各个平台深度思考模型
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+114
@@ -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);
|
||||
}
|
||||
}
|
||||
+80
@@ -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;
|
||||
}
|
||||
}
|
||||
+130
@@ -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);
|
||||
}
|
||||
}
|
||||
+13
@@ -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;
|
||||
}
|
||||
+44
@@ -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;
|
||||
}
|
||||
}
|
||||
+10
@@ -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);
|
||||
}
|
||||
+20
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
+18
@@ -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;
|
||||
}
|
||||
}
|
||||
+20
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.luohuo.flex.ai.core.model.strategy.chat;
|
||||
|
||||
public class ReasoningChunk {
|
||||
public String content;
|
||||
public String reasoning;
|
||||
}
|
||||
+283
-88
@@ -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();
|
||||
|
||||
+1
@@ -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());
|
||||
|
||||
+11
-6
@@ -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);
|
||||
}
|
||||
|
||||
+13
@@ -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;
|
||||
}
|
||||
+2
@@ -15,4 +15,6 @@ public class Init implements Serializable {
|
||||
private String roomGroupId;
|
||||
// 七牛云配置
|
||||
private QiNiu qiNiu;
|
||||
// ICE Server 配置
|
||||
private IceServer iceServer;
|
||||
}
|
||||
|
||||
-2
@@ -43,6 +43,4 @@ public interface SysConfigService {
|
||||
* 获取系统全局配置
|
||||
*/
|
||||
Init getSystemInit();
|
||||
|
||||
<T> T getBeanByName(String name, Class<T> t);
|
||||
}
|
||||
|
||||
+30
-89
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+36
-49
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+60
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -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);
|
||||
}
|
||||
+229
@@ -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();
|
||||
}
|
||||
}
|
||||
+13
-4
@@ -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")
|
||||
|
||||
+85
@@ -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();
|
||||
}
|
||||
}
|
||||
+11
@@ -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
|
||||
}
|
||||
+11
@@ -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;
|
||||
}
|
||||
+13
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user