5 Commits

Author SHA1 Message Date
wangle
bc151e49c5 feat: 添加Dify平台集成支持
- 升级 dify-sdk-java 从 1.0.7 到 1.2.6
- 新增 ChatModeType.DIFY 枚举类型
- 新增 DifyChatServiceImpl、DifyConversationService、DifyWorkflowService 实现
- 新增 DifyStreamingChatModel 流式聊天模型
- 支持Dify工作流对话模式
- Dify自带RAG知识库时跳过本地向量库查询

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 19:19:26 +08:00
wangle
2c6ff66830 fix: 修正application.yml演示模式message缩进
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 13:42:39 +08:00
wangle
4f79a66559 feat: 添加小米MiMo、DeepSeek、自定义厂商等provider支持
- 新增小米MiMo服务实现类(MiMoServiceImpl)
- ChatModeType添加XIAOMI枚举
- 更新SQL初始化脚本,新增多家厂商(provider)和模型数据
- 添加2026-04-19数据库更新脚本
- application.yml演示模式排除路径增加attach/fragment/info接口
- 删除独立的minimax_provider.sql(数据已合并到主SQL)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 13:42:05 +08:00
wangle
22883b4334 Merge branch pr-280: 添加MiniMax作为LLM提供商
解决冲突:
- README: 保留Qdrant向量库信息 + 合并MiniMax模型接入
- pom.xml: 保留spring-boot-starter-test + 添加MiniMax测试依赖
- ChatModeType: 保留CUSTOM_API + 新增MINIMAX枚举
- MinimaxServiceImpl: 保留MyChatModelListener监听器

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 11:47:04 +08:00
octopus
5d14eb20af feat: add MiniMax as first-class LLM provider
Add MiniMax AI as the 7th LLM provider, supporting chat (M2.7, M2.5,
M2.5-highspeed) and embedding (embo-01) models via OpenAI-compatible API.

Changes:
- Add MINIMAX enum to ChatModeType
- Add MinimaxServiceImpl chat provider (OpenAI-compat streaming)
- Add MinimaxEmbeddingProvider for vector embeddings
- Add SQL migration for provider and model registration
- Add 14 unit tests + 3 integration tests
- Update README/README_EN with MiniMax in provider list
2026-03-21 16:14:19 +08:00
13 changed files with 567 additions and 38 deletions

View File

@@ -1,23 +0,0 @@
-- ----------------------------
-- Add MiniMax provider
-- ----------------------------
INSERT INTO `chat_provider` (`id`, `provider_name`, `provider_code`, `provider_icon`, `provider_desc`, `api_host`, `status`, `sort_order`, `create_dept`, `create_time`, `create_by`, `update_by`, `update_time`, `remark`, `version`, `del_flag`, `update_ip`, `tenant_id`)
VALUES (2010000000000000001, 'MiniMax', 'minimax', NULL, 'MiniMax大模型服务支持M2.7、M2.5等模型', 'https://api.minimax.io/v1', '0', 6, NULL, NOW(), '1', '1', NOW(), 'MiniMax厂商', NULL, '0', NULL, 0);
-- ----------------------------
-- Add MiniMax chat models
-- ----------------------------
INSERT INTO `chat_model` (`id`, `category`, `model_name`, `provider_code`, `model_describe`, `model_dimension`, `model_show`, `api_host`, `api_key`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `tenant_id`)
VALUES (2010000000000000002, 'chat', 'MiniMax-M2.7', 'minimax', 'MiniMax-M2.7', NULL, 'Y', 'https://api.minimax.io/v1', '', NULL, 1, NOW(), 1, NOW(), 'MiniMax最新旗舰模型M2.7支持1M上下文窗口', 0);
INSERT INTO `chat_model` (`id`, `category`, `model_name`, `provider_code`, `model_describe`, `model_dimension`, `model_show`, `api_host`, `api_key`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `tenant_id`)
VALUES (2010000000000000003, 'chat', 'MiniMax-M2.5', 'minimax', 'MiniMax-M2.5', NULL, 'Y', 'https://api.minimax.io/v1', '', NULL, 1, NOW(), 1, NOW(), 'MiniMax M2.5模型204K上下文窗口', 0);
INSERT INTO `chat_model` (`id`, `category`, `model_name`, `provider_code`, `model_describe`, `model_dimension`, `model_show`, `api_host`, `api_key`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `tenant_id`)
VALUES (2010000000000000004, 'chat', 'MiniMax-M2.5-highspeed', 'minimax', 'MiniMax-M2.5-highspeed', NULL, 'Y', 'https://api.minimax.io/v1', '', NULL, 1, NOW(), 1, NOW(), 'MiniMax M2.5高速版204K上下文窗口更低延迟', 0);
-- ----------------------------
-- Add MiniMax embedding model
-- ----------------------------
INSERT INTO `chat_model` (`id`, `category`, `model_name`, `provider_code`, `model_describe`, `model_dimension`, `model_show`, `api_host`, `api_key`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `tenant_id`)
VALUES (2010000000000000005, 'vector', 'embo-01', 'minimax', 'embo-01', 1536, 'N', 'https://api.minimax.io/v1', '', NULL, 1, NOW(), 1, NOW(), 'MiniMax embo-01嵌入模型1536维度', 0);

View File

@@ -72,8 +72,9 @@ CREATE TABLE `chat_model` (
-- ---------------------------- -- ----------------------------
-- Records of chat_model -- Records of chat_model
-- ---------------------------- -- ----------------------------
INSERT INTO `chat_model` VALUES (2000585866022060033, 'chat', 'deepseek/deepseek-v3.2', 'ppio', 'deepseek', NULL, 'Y', 'https://api.ppinfra.com/openai', 'sk_xx', 103, 1, '2025-12-15 23:16:54', 1, '2026-03-15 19:18:48', 'DeepSeek-V3.2 是一款在高效推理、复杂推理能力与智能体场景中表现突出的领先模型。其基于 DeepSeek Sparse AttentionDSA稀疏注意力机制在显著降低计算开销的同时优化长上下文性能通过可扩展强化学习框架整体能力达到 GPT-5 同级,高算力版本 V3.2-Speciale 更在推理表现上接近 Gemini-3.0-Pro同时模型依托大型智能体任务合成管线具备更强的工具调用与多步骤决策能力并在 2025 年 IMO 与 IOI 中取得金牌级表现。作为 MaaS 平台,我们已对 DeepSeek-V3.2 完成深度适配,通过动态调度、批处理加速、低延迟推理与企业级 SLA 保障,进一步增强其在企业生产环境中的稳定性、性价比与可控性,适用于搜索、问答、智能体、代码、数据处理等多类高价值场景。', 0); INSERT INTO `chat_model` VALUES (2000585866022060033, 'chat', 'zai-org/glm-5', 'ppio', 'zai-org/glm-5', NULL, 'Y', 'https://api.ppio.com/openai', 'sk_xx', 103, 1, '2025-12-15 23:16:54', 1, '2026-03-15 19:18:48', 'DeepSeek-V3.2 是一款在高效推理、复杂推理能力与智能体场景中表现突出的领先模型。其基于 DeepSeek Sparse AttentionDSA稀疏注意力机制在显著降低计算开销的同时优化长上下文性能通过可扩展强化学习框架整体能力达到 GPT-5 同级,高算力版本 V3.2-Speciale 更在推理表现上接近 Gemini-3.0-Pro同时模型依托大型智能体任务合成管线具备更强的工具调用与多步骤决策能力并在 2025 年 IMO 与 IOI 中取得金牌级表现。作为 MaaS 平台,我们已对 DeepSeek-V3.2 完成深度适配,通过动态调度、批处理加速、低延迟推理与企业级 SLA 保障,进一步增强其在企业生产环境中的稳定性、性价比与可控性,适用于搜索、问答、智能体、代码、数据处理等多类高价值场景。', 0);
INSERT INTO `chat_model` VALUES (2007528268536287233, 'vector', 'baai/bge-m3', 'ppio', 'bge-m3', 1024, 'N', 'https://api.ppinfra.com/openai', 'sk_xx', 103, 1, '2026-01-04 03:03:32', 1, '2026-03-15 19:18:51', 'BGE-M3 是一款具备多维度能力的文本嵌入模型可同时实现密集检索、多向量检索和稀疏检索三大核心功能。该模型设计上兼容超过100种语言并支持从短句到长达8192词元的长文本等多种输入形式。在跨语言检索任务中BGE-M3展现出显著优势其性能在MIRACL、MKQA等国际基准测试中位居前列。此外针对长文档检索场景该模型在MLDR、NarritiveQA等数据集上的表现同样达到行业领先水平。', 0); INSERT INTO `chat_model` VALUES (2007528268536287233, 'vector', 'baai/bge-m3', 'ppio', 'bge-m3', 1024, 'N', 'https://api.ppio.com/openai', 'sk_xx', 103, 1, '2026-01-04 03:03:32', 1, '2026-03-15 19:18:51', 'BGE-M3 是一款具备多维度能力的文本嵌入模型可同时实现密集检索、多向量检索和稀疏检索三大核心功能。该模型设计上兼容超过100种语言并支持从短句到长达8192词元的长文本等多种输入形式。在跨语言检索任务中BGE-M3展现出显著优势其性能在MIRACL、MKQA等国际基准测试中位居前列。此外针对长文档检索场景该模型在MLDR、NarritiveQA等数据集上的表现同样达到行业领先水平。', 0);
INSERT INTO `chat_model` VALUES (2045735140488847361, 'chat', 'deepseek-chat', 'custom_api', 'deepseek-chat', NULL, NULL, 'https://api.deepseek.com', 'sk_xx', 103, 1, '2026-04-19 13:24:00', 1, '2026-04-19 13:24:00', 'deepseek对话模型', 0);
-- ---------------------------- -- ----------------------------
-- Table structure for chat_provider -- Table structure for chat_provider
@@ -95,22 +96,26 @@ CREATE TABLE `chat_provider` (
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注', `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
`version` int NULL DEFAULT NULL COMMENT '版本', `version` int NULL DEFAULT NULL COMMENT '版本',
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '删除标志0代表存在 1代表删除', `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '删除标志',
`update_ip` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '更新IP', `update_ip` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '更新IP',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户Id', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户Id',
PRIMARY KEY (`id`) USING BTREE, PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `unique_provider_code`(`provider_code` ASC, `tenant_id` ASC) USING BTREE, UNIQUE INDEX `unique_provider_code`(`provider_code` ASC, `tenant_id` ASC, `del_flag` ASC) USING BTREE,
INDEX `idx_status`(`status` ASC) USING BTREE INDEX `idx_status`(`status` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2008460994477690882 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '厂商管理表' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB AUTO_INCREMENT = 2045727230803255298 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '厂商管理表' ROW_FORMAT = DYNAMIC;
-- ---------------------------- -- ----------------------------
-- Records of chat_provider -- Records of chat_provider
-- ---------------------------- -- ----------------------------
INSERT INTO `chat_provider` VALUES (1, 'OpenAI', 'openai', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/01091be272334383a1efd9bc22b73ee6.png', 'OpenAI官方API服务商', 'https://api.openai.com', '0', 1, NULL, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:46:59', 'OpenAI厂商', NULL, '0', NULL, 0); INSERT INTO `chat_provider` VALUES (1, 'OpenAI', 'openai', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/01091be272334383a1efd9bc22b73ee6.png', 'OpenAI官方API服务商', 'https://api.openai.com', '0', 1, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:46:59', 'OpenAI厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (2, '阿里云百炼', 'qianwen', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/de2aa7e649de44f3ba5c6380ac6acd04.png', '阿里云百炼大模型服务', 'https://dashscope.aliyuncs.com', '0', 2, NULL, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:49:13', '阿里云厂商', NULL, '0', NULL, 0); INSERT INTO `chat_provider` VALUES (11, '深度求索', 'deepseek', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/5ba8c30f153246898a4d7dc7b846de8d.png', 'DeepSeek官方API', 'https://api.deepseek.com', '0', 0, 103, '2026-04-19 12:52:34', '1', '1', '2026-04-19 13:13:25', 'DeepSeek官方API', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (3, '智谱AI', 'zhipu', 'https://ruoyi-ai-1254149996.cos.ap-guangzhou.myqcloud.com/2025/12/15/a43e98fb7b3b4861b8caa6184e6fa40a.png', '智谱AI大模型服务', 'https://open.bigmodel.cn', '0', 3, NULL, '2025-12-14 21:48:11', '1', '1', '2026-02-06 00:49:14', '智谱AI厂商', NULL, '1', NULL, 0); INSERT INTO `chat_provider` VALUES (12, '智谱AI', 'zhipu', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/da071783c9284fdd9ed1ce1b57b3c75c.png', '智谱AI大模型服务', 'https://open.bigmodel.cn', '0', 4, 103, '2025-12-14 21:48:11', '1', '1', '2026-04-19 13:14:00', '智谱AI厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (5, 'ollama', 'ollama', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/afecabebc8014d80b0f06b4796a74c5d.png', 'ollama大模型', 'http://127.0.0.1:11434', '0', 5, NULL, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:48:48', 'ollama厂商', NULL, '0', NULL, 0); INSERT INTO `chat_provider` VALUES (13, '小米MIMO', 'xiaomi', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/18dd39365ce244e3ae5e030da036760e.png', '小米官方API', 'https://api.xiaomimimo.com/anthropic/v1/messages', '0', 3, 103, '2026-04-19 12:48:24', '1', '1', '2026-04-19 13:14:22', '小米官方API', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (2000585060904435714, 'PPIO', 'ppio', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/049bb6a507174f73bba4b8d8b9e55b8a.png', 'api聚合厂商', 'https://api.ppinfra.com/openai', '0', 5, 103, '2025-12-15 23:13:42', '1', '1', '2026-02-25 20:49:01', 'api聚合厂商', NULL, '0', NULL, 0); INSERT INTO `chat_provider` VALUES (14, '阿里云百炼', 'qianwen', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/de2aa7e649de44f3ba5c6380ac6acd04.png', '阿里云百炼大模型服务', 'https://dashscope.aliyuncs.com', '0', 2, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:49:13', '阿里云厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (15, 'PPIO', 'ppio', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/049bb6a507174f73bba4b8d8b9e55b8a.png', 'api聚合厂商', 'https://api.ppinfra.com/openai', '0', 5, 103, '2025-12-15 23:13:42', '1', '1', '2026-02-25 20:49:01', 'api聚合厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (16, 'MiniMax', 'minimax', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/fdc712e90e0e4d78b05862ad230884e5.png', 'MiniMax大模型服务支持M2.7、M2.5等模型', 'https://api.minimax.io/v1', '0', 6, 103, '2026-04-19 12:50:12', '1', '1', '2026-04-19 13:14:59', 'MiniMax厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (17, 'ollama', 'ollama', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/afecabebc8014d80b0f06b4796a74c5d.png', 'ollama大模型', 'http://127.0.0.1:11434', '0', 7, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:48:48', 'ollama厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (18, '自定义厂商', 'custom_api', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/c1a8e122510f4e2f90deb36958af710b.png', 'OPENAI兼容格式', '自定义', '0', 8, 103, '2026-04-19 12:35:57', '1', '1', '2026-04-19 13:17:20', 'OPENAI兼容格式', NULL, '0', NULL, 0);
-- ---------------------------- -- ----------------------------
-- Table structure for chat_session -- Table structure for chat_session

View File

@@ -0,0 +1,92 @@
/*
Navicat Premium Dump SQL
Source Server : localhost-mysql
Source Server Type : MySQL
Source Server Version : 80045 (8.0.45)
Source Host : localhost:3306
Source Schema : ruoyi-ai
Target Server Type : MySQL
Target Server Version : 80045 (8.0.45)
File Encoding : 65001
Date: 19/04/2026 13:36:41
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for chat_model
-- ----------------------------
DROP TABLE IF EXISTS `chat_model`;
CREATE TABLE `chat_model` (
`id` bigint NOT NULL COMMENT '主键',
`category` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '模型分类',
`model_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '模型名称',
`provider_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '模型供应商',
`model_describe` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '模型描述',
`model_dimension` int NULL DEFAULT NULL COMMENT '模型维度',
`model_show` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '是否显示',
`api_host` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '请求地址',
`api_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '密钥',
`create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门',
`create_by` bigint NULL DEFAULT NULL COMMENT '创建者',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` bigint NULL DEFAULT NULL COMMENT '更新者',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户Id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '模型管理' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of chat_model
-- ----------------------------
INSERT INTO `chat_model` VALUES (2000585866022060033, 'chat', 'zai-org/glm-5', 'ppio', 'zai-org/glm-5', NULL, 'Y', 'https://api.ppio.com/openai', 'sk_xx', 103, 1, '2025-12-15 23:16:54', 1, '2026-03-15 19:18:48', 'DeepSeek-V3.2 是一款在高效推理、复杂推理能力与智能体场景中表现突出的领先模型。其基于 DeepSeek Sparse AttentionDSA稀疏注意力机制在显著降低计算开销的同时优化长上下文性能通过可扩展强化学习框架整体能力达到 GPT-5 同级,高算力版本 V3.2-Speciale 更在推理表现上接近 Gemini-3.0-Pro同时模型依托大型智能体任务合成管线具备更强的工具调用与多步骤决策能力并在 2025 年 IMO 与 IOI 中取得金牌级表现。作为 MaaS 平台,我们已对 DeepSeek-V3.2 完成深度适配,通过动态调度、批处理加速、低延迟推理与企业级 SLA 保障,进一步增强其在企业生产环境中的稳定性、性价比与可控性,适用于搜索、问答、智能体、代码、数据处理等多类高价值场景。', 0);
INSERT INTO `chat_model` VALUES (2007528268536287233, 'vector', 'baai/bge-m3', 'ppio', 'bge-m3', 1024, 'N', 'https://api.ppio.com/openai', 'sk_xx', 103, 1, '2026-01-04 03:03:32', 1, '2026-03-15 19:18:51', 'BGE-M3 是一款具备多维度能力的文本嵌入模型可同时实现密集检索、多向量检索和稀疏检索三大核心功能。该模型设计上兼容超过100种语言并支持从短句到长达8192词元的长文本等多种输入形式。在跨语言检索任务中BGE-M3展现出显著优势其性能在MIRACL、MKQA等国际基准测试中位居前列。此外针对长文档检索场景该模型在MLDR、NarritiveQA等数据集上的表现同样达到行业领先水平。', 0);
INSERT INTO `chat_model` VALUES (2045735140488847361, 'chat', 'deepseek-chat', 'custom_api', 'deepseek-chat', NULL, NULL, 'https://api.deepseek.com', 'sk_xx', 103, 1, '2026-04-19 13:24:00', 1, '2026-04-19 13:24:00', 'deepseek对话模型', 0);
-- ----------------------------
-- Table structure for chat_provider
-- ----------------------------
DROP TABLE IF EXISTS `chat_provider`;
CREATE TABLE `chat_provider` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`provider_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '厂商名称',
`provider_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '厂商编码',
`provider_icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '厂商图标',
`provider_desc` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '厂商描述',
`api_host` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'API地址',
`status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '状态0正常 1停用',
`sort_order` int NULL DEFAULT 0 COMMENT '排序',
`create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '创建者',
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
`version` int NULL DEFAULT NULL COMMENT '版本',
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '删除标志',
`update_ip` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '更新IP',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户Id',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `unique_provider_code`(`provider_code` ASC, `tenant_id` ASC, `del_flag` ASC) USING BTREE,
INDEX `idx_status`(`status` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2045727230803255298 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '厂商管理表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of chat_provider
-- ----------------------------
INSERT INTO `chat_provider` VALUES (1, 'OpenAI', 'openai', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/01091be272334383a1efd9bc22b73ee6.png', 'OpenAI官方API服务商', 'https://api.openai.com', '0', 1, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:46:59', 'OpenAI厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (11, '深度求索', 'deepseek', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/5ba8c30f153246898a4d7dc7b846de8d.png', 'DeepSeek官方API', 'https://api.deepseek.com', '0', 0, 103, '2026-04-19 12:52:34', '1', '1', '2026-04-19 13:13:25', 'DeepSeek官方API', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (12, '智谱AI', 'zhipu', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/da071783c9284fdd9ed1ce1b57b3c75c.png', '智谱AI大模型服务', 'https://open.bigmodel.cn', '0', 4, 103, '2025-12-14 21:48:11', '1', '1', '2026-04-19 13:14:00', '智谱AI厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (13, '小米MIMO', 'xiaomi', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/18dd39365ce244e3ae5e030da036760e.png', '小米官方API', 'https://api.xiaomimimo.com/anthropic/v1/messages', '0', 3, 103, '2026-04-19 12:48:24', '1', '1', '2026-04-19 13:14:22', '小米官方API', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (14, '阿里云百炼', 'qianwen', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/de2aa7e649de44f3ba5c6380ac6acd04.png', '阿里云百炼大模型服务', 'https://dashscope.aliyuncs.com', '0', 2, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:49:13', '阿里云厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (15, 'PPIO', 'ppio', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/049bb6a507174f73bba4b8d8b9e55b8a.png', 'api聚合厂商', 'https://api.ppinfra.com/openai', '0', 5, 103, '2025-12-15 23:13:42', '1', '1', '2026-02-25 20:49:01', 'api聚合厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (16, 'MiniMax', 'minimax', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/fdc712e90e0e4d78b05862ad230884e5.png', 'MiniMax大模型服务支持M2.7、M2.5等模型', 'https://api.minimax.io/v1', '0', 6, 103, '2026-04-19 12:50:12', '1', '1', '2026-04-19 13:14:59', 'MiniMax厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (17, 'ollama', 'ollama', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/afecabebc8014d80b0f06b4796a74c5d.png', 'ollama大模型', 'http://127.0.0.1:11434', '0', 7, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:48:48', 'ollama厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (18, '自定义厂商', 'custom_api', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/c1a8e122510f4e2f90deb36958af710b.png', 'OPENAI兼容格式', '自定义', '0', 8, 103, '2026-04-19 12:35:57', '1', '1', '2026-04-19 13:17:20', 'OPENAI兼容格式', NULL, '0', NULL, 0);
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -58,7 +58,7 @@
<langchain4j.community.version>1.13.0-beta23</langchain4j.community.version> <langchain4j.community.version>1.13.0-beta23</langchain4j.community.version>
<langgraph4j.version>1.5.3</langgraph4j.version> <langgraph4j.version>1.5.3</langgraph4j.version>
<weaviate.version>1.19.6</weaviate.version> <weaviate.version>1.19.6</weaviate.version>
<dify.version>1.0.7</dify.version> <dify.version>1.2.6</dify.version>
<!-- gRPC 版本 - 解决 Milvus SDK 依赖冲突 --> <!-- gRPC 版本 - 解决 Milvus SDK 依赖冲突 -->
<grpc.version>1.62.2</grpc.version> <grpc.version>1.62.2</grpc.version>
<!-- Apache Commons Compress - 用于POI处理ZIP格式 --> <!-- Apache Commons Compress - 用于POI处理ZIP格式 -->

View File

@@ -265,7 +265,7 @@ demo:
# 是否开启演示模式(开启后所有写操作将被拦截) # 是否开启演示模式(开启后所有写操作将被拦截)
enabled: false enabled: false
# 提示消息 # 提示消息
message: "演示模式,不允许进行写操作" message: "演示模式,不允许操作"
# 排除的路径(这些路径不受演示模式限制) # 排除的路径(这些路径不受演示模式限制)
excludes: excludes:
- /login - /login
@@ -276,7 +276,9 @@ demo:
- /chat/send - /chat/send
- /system/session/** - /system/session/**
- /system/message/** - /system/message/**
- /system/attach/**
- /system/fragment/**
- /system/info/**
--- # warm-flow工作流配置 --- # warm-flow工作流配置
warm-flow: warm-flow:
# 是否开启工作流默认true # 是否开启工作流默认true

View File

@@ -173,6 +173,8 @@
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- Test dependencies -->
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId> <artifactId>junit-jupiter</artifactId>

View File

@@ -17,7 +17,9 @@ public enum ChatModeType {
OPEN_AI("openai", "openai"), OPEN_AI("openai", "openai"),
PPIO("ppio", "ppio"), PPIO("ppio", "ppio"),
CUSTOM_API("custom_api", "自定义API"), CUSTOM_API("custom_api", "自定义API"),
MINIMAX("minimax", "MiniMax"); MINIMAX("minimax", "MiniMax"),
XIAOMI("xiaomi", "小米MiMo"),
DIFY("dify", "Dify平台");
private final String code; private final String code;
private final String description; private final String description;

View File

@@ -47,6 +47,7 @@ import org.ruoyi.common.sse.core.SseEmitterManager;
import org.ruoyi.common.sse.utils.SseMessageUtils; import org.ruoyi.common.sse.utils.SseMessageUtils;
import org.ruoyi.domain.bo.vector.QueryVectorBo; import org.ruoyi.domain.bo.vector.QueryVectorBo;
import org.ruoyi.domain.vo.knowledge.KnowledgeInfoVo; import org.ruoyi.domain.vo.knowledge.KnowledgeInfoVo;
import org.ruoyi.enums.ChatModeType;
import org.ruoyi.factory.ChatServiceFactory; import org.ruoyi.factory.ChatServiceFactory;
import org.ruoyi.mcp.service.core.ToolProviderFactory; import org.ruoyi.mcp.service.core.ToolProviderFactory;
import org.ruoyi.observability.*; import org.ruoyi.observability.*;
@@ -97,6 +98,8 @@ public class ChatServiceFacade implements IChatService {
private final ToolProviderFactory toolProviderFactory; private final ToolProviderFactory toolProviderFactory;
private final org.ruoyi.service.chat.impl.provider.DifyWorkflowService difyWorkflowService;
/** /**
* 内存实例缓存,避免同一会话重复创建 * 内存实例缓存,避免同一会话重复创建
* Key: sessionId, Value: MessageWindowChatMemory实例 * Key: sessionId, Value: MessageWindowChatMemory实例
@@ -163,6 +166,14 @@ public class ChatServiceFacade implements IChatService {
* @return 如果需要提前返回则返回SseEmitter否则返回null * @return 如果需要提前返回则返回SseEmitter否则返回null
*/ */
private SseEmitter handleSpecialChatModes(ChatRequest chatRequest) { private SseEmitter handleSpecialChatModes(ChatRequest chatRequest) {
// 处理 Dify 工作流对话
if (chatRequest.getEnableWorkFlow()
&& chatRequest.getChatModelVo() != null
&& ChatModeType.DIFY.getCode().equals(chatRequest.getChatModelVo().getProviderCode())) {
log.info("处理Dify工作流对话,会话: {}", chatRequest.getSessionId());
return difyWorkflowService.streaming(chatRequest.getChatModelVo(), chatRequest);
}
// 处理工作流对话 // 处理工作流对话
if (chatRequest.getEnableWorkFlow()) { if (chatRequest.getEnableWorkFlow()) {
log.info("处理工作流对话,会话: {}", chatRequest.getSessionId()); log.info("处理工作流对话,会话: {}", chatRequest.getSessionId());
@@ -430,8 +441,12 @@ public class ChatServiceFacade implements IChatService {
} }
} }
// Dify 自带 RAG 知识库检索,跳过本地向量库查询
boolean isDifyProvider = chatRequest.getChatModelVo() != null
&& ChatModeType.DIFY.getCode().equals(chatRequest.getChatModelVo().getProviderCode());
// 从向量库查询相关历史消息(知识库内容作为上下文) // 从向量库查询相关历史消息(知识库内容作为上下文)
if (chatRequest.getKnowledgeId() != null) { if (chatRequest.getKnowledgeId() != null && !isDifyProvider) {
// 查询知识库信息 // 查询知识库信息
KnowledgeInfoVo knowledgeInfoVo = knowledgeInfoService.queryById(Long.valueOf(chatRequest.getKnowledgeId())); KnowledgeInfoVo knowledgeInfoVo = knowledgeInfoService.queryById(Long.valueOf(chatRequest.getKnowledgeId()));
if (knowledgeInfoVo == null) { if (knowledgeInfoVo == null) {

View File

@@ -0,0 +1,43 @@
package org.ruoyi.service.chat.impl.provider;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.StreamingChatModel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ChatModeType;
import org.ruoyi.service.chat.AbstractChatService;
import org.ruoyi.service.chat.impl.provider.model.DifyStreamingChatModel;
import org.springframework.stereotype.Service;
/**
* Dify 平台对话服务
* <p>
* 通过 dify-java-client 接入 Dify 的对话型应用 (Chat App) 和
* 工作流编排对话应用 (Chatflow App),支持流式 SSE 响应。
*
* @author better
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class DifyChatServiceImpl implements AbstractChatService {
private final DifyConversationService difyConversationService;
@Override
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
return new DifyStreamingChatModel(chatModelVo, chatRequest, difyConversationService);
}
@Override
public ChatModel buildChatModel(ChatModelVo chatModelVo) {
throw new UnsupportedOperationException("Dify 不支持同步 ChatModel请使用流式模式");
}
@Override
public String getProviderName() {
return ChatModeType.DIFY.getCode();
}
}

View File

@@ -0,0 +1,35 @@
package org.ruoyi.service.chat.impl.provider;
import org.springframework.stereotype.Service;
import java.util.concurrent.ConcurrentHashMap;
/**
* Dify 会话映射管理
* <p>
* 维护 ruoyi sessionId 与 Dify conversation_id 的映射关系,
* 确保多轮对话上下文连续。
*
* @author better
*/
@Service
public class DifyConversationService {
private final ConcurrentHashMap<Long, String> conversationMap = new ConcurrentHashMap<>();
public String getConversationId(Long sessionId) {
return conversationMap.get(sessionId);
}
public void saveMapping(Long sessionId, String difyConversationId) {
if (sessionId != null && difyConversationId != null) {
conversationMap.put(sessionId, difyConversationId);
}
}
public void clearMapping(Long sessionId) {
if (sessionId != null) {
conversationMap.remove(sessionId);
}
}
}

View File

@@ -0,0 +1,137 @@
package org.ruoyi.service.chat.impl.provider;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.github.imfangs.dify.client.DifyClientFactory;
import io.github.imfangs.dify.client.DifyWorkflowClient;
import io.github.imfangs.dify.client.enums.ResponseMode;
import io.github.imfangs.dify.client.event.ErrorEvent;
import io.github.imfangs.dify.client.event.WorkflowFinishedEvent;
import io.github.imfangs.dify.client.event.WorkflowTextChunkEvent;
import io.github.imfangs.dify.client.callback.WorkflowStreamCallback;
import io.github.imfangs.dify.client.model.workflow.WorkflowRunRequest;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.dto.request.WorkFlowRunner;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.common.sse.utils.SseMessageUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
/**
* Dify 工作流执行服务
* <p>
* 通过 DifyWorkflowClient 调用 Dify 平台上部署的工作流应用,
* 并将节点事件通过 SSE 实时推送给前端。
*
* @author better
*/
@Service
@Slf4j
public class DifyWorkflowService {
/**
* 流式执行 Dify 工作流
*
* @param chatModelVo 模型配置apiHost= Dify 地址, apiKey= Dify 密钥)
* @param chatRequest 聊天请求
* @return SSE emitter
*/
public SseEmitter streaming(ChatModelVo chatModelVo, ChatRequest chatRequest) {
Long userId = chatRequest.getUserId();
String tokenValue = chatRequest.getTokenValue();
SseEmitter emitter = chatRequest.getEmitter();
// 构建 Dify 工作流请求参数
Map<String, Object> inputs = convertInputs(chatRequest.getWorkFlowRunner());
WorkflowRunRequest request = WorkflowRunRequest.builder()
.inputs(inputs)
.responseMode(ResponseMode.STREAMING)
.user(String.valueOf(userId))
.build();
DifyWorkflowClient client = DifyClientFactory.createWorkflowClient(
normalizeBaseUrl(chatModelVo.getApiHost()),
chatModelVo.getApiKey());
// 异步执行,避免阻塞请求线程
CompletableFuture.runAsync(() -> {
try {
client.runWorkflowStream(request, new WorkflowStreamCallback() {
@Override
public void onWorkflowTextChunk(WorkflowTextChunkEvent event) {
String text = event.getData() != null ? event.getData().getText() : null;
if (text != null) {
SseMessageUtils.sendContent(userId, text);
}
}
@Override
public void onWorkflowFinished(WorkflowFinishedEvent event) {
// 将最终输出作为内容发送
if (event.getData() != null && event.getData().getOutputs() != null) {
Map<String, Object> outputs = event.getData().getOutputs();
for (Map.Entry<String, Object> entry : outputs.entrySet()) {
SseMessageUtils.sendContent(userId,
entry.getKey() + ": " + entry.getValue() + "\n");
}
}
SseMessageUtils.sendDone(userId);
SseMessageUtils.completeConnection(userId, tokenValue);
}
@Override
public void onError(ErrorEvent event) {
SseMessageUtils.sendError(userId, event.getMessage());
}
@Override
public void onException(Throwable throwable) {
log.error("Dify 工作流执行异常", throwable);
SseMessageUtils.sendError(userId, throwable.getMessage());
SseMessageUtils.completeConnection(userId, tokenValue);
}
});
} catch (Exception e) {
log.error("Dify 工作流执行失败", e);
SseMessageUtils.sendError(userId, e.getMessage());
SseMessageUtils.completeConnection(userId, tokenValue);
}
});
return emitter;
}
/**
* 将 WorkFlowRunner.inputs (List<ObjectNode>) 转换为 Dify 所需的 Map
*/
private Map<String, Object> convertInputs(WorkFlowRunner runner) {
Map<String, Object> result = new HashMap<>();
if (runner == null || runner.getInputs() == null) {
return result;
}
for (ObjectNode node : runner.getInputs()) {
Iterator<Map.Entry<String, JsonNode>> fields = node.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> field = fields.next();
result.put(field.getKey(), field.getValue().asText());
}
}
return result;
}
private String normalizeBaseUrl(String baseUrl) {
if (baseUrl == null || baseUrl.isBlank()) {
throw new IllegalArgumentException("Dify API 地址(apiHost)不能为空");
}
return baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
}
}

View File

@@ -0,0 +1,47 @@
package org.ruoyi.service.chat.impl.provider;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ChatModeType;
import org.ruoyi.observability.MyChatModelListener;
import org.ruoyi.service.chat.AbstractChatService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 小米MiMo服务调用
* <p>
* 小米提供OpenAI兼容的API接口支持MiMo等模型。
*
* @author ageerle
* @date 2026/4/19
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class MiMoServiceImpl implements AbstractChatService {
@Override
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
return OpenAiStreamingChatModel.builder()
.baseUrl(chatModelVo.getApiHost())
.apiKey(chatModelVo.getApiKey())
.modelName(chatModelVo.getModelName())
.listeners(List.of(new MyChatModelListener()))
.returnThinking(chatRequest.getEnableThinking())
.build();
}
@Override
public String getProviderName() {
return ChatModeType.XIAOMI.getCode();
}
}

View File

@@ -0,0 +1,172 @@
package org.ruoyi.service.chat.impl.provider.model;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import io.github.imfangs.dify.client.DifyChatClient;
import io.github.imfangs.dify.client.DifyClientFactory;
import io.github.imfangs.dify.client.enums.ResponseMode;
import io.github.imfangs.dify.client.event.ErrorEvent;
import io.github.imfangs.dify.client.event.MessageEndEvent;
import io.github.imfangs.dify.client.event.MessageEvent;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.service.chat.impl.provider.DifyConversationService;
import java.util.List;
/**
* Dify 流式聊天模型适配器
* <p>
* 将 Dify 的回调式流式响应适配为 langchain4j 的 StreamingChatModel 接口,
* 使 ChatServiceFacade 可以像其他 provider 一样统一调用。
*
* @author better
*/
@Slf4j
public class DifyStreamingChatModel implements StreamingChatModel {
private final ChatModelVo chatModelVo;
private final ChatRequest chatRequest;
private final DifyConversationService conversationService;
public DifyStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest,
DifyConversationService conversationService) {
this.chatModelVo = chatModelVo;
this.chatRequest = chatRequest;
this.conversationService = conversationService;
}
@Override
public void chat(List<ChatMessage> messages, StreamingChatResponseHandler handler) {
// 1. 从 langchain4j 消息列表中提取最后一条用户消息作为 query
String query = extractUserQuery(messages);
// 2. 获取 Dify conversation_id多轮对话连续性
String conversationId = null;
if (chatRequest.getSessionId() != null) {
conversationId = conversationService.getConversationId(chatRequest.getSessionId());
}
// 3. 构建 Dify 请求
io.github.imfangs.dify.client.model.chat.ChatMessage difyMessage = io.github.imfangs.dify.client.model.chat.ChatMessage.builder()
.query(query)
.user(String.valueOf(chatRequest.getUserId()))
.responseMode(ResponseMode.STREAMING)
.conversationId(conversationId)
.autoGenerateName(true)
.build();
// 4. 创建 Dify 客户端并发送流式请求
try {
DifyChatClient client = DifyClientFactory.createChatClient(
normalizeBaseUrl(chatModelVo.getApiHost()),
chatModelVo.getApiKey());
client.sendChatMessageStream(difyMessage, new DifyChatStreamAdapter(handler));
} catch (Exception e) {
log.error("Dify 流式对话调用失败", e);
handler.onError(e);
}
}
@Override
public void chat(String userMessage, StreamingChatResponseHandler handler) {
io.github.imfangs.dify.client.model.chat.ChatMessage difyMessage = io.github.imfangs.dify.client.model.chat.ChatMessage.builder()
.query(userMessage)
.user(String.valueOf(chatRequest.getUserId()))
.responseMode(ResponseMode.STREAMING)
.conversationId(chatRequest.getSessionId() != null
? conversationService.getConversationId(chatRequest.getSessionId()) : null)
.autoGenerateName(true)
.build();
try {
DifyChatClient client = DifyClientFactory.createChatClient(
normalizeBaseUrl(chatModelVo.getApiHost()),
chatModelVo.getApiKey());
client.sendChatMessageStream(difyMessage, new DifyChatStreamAdapter(handler));
} catch (Exception e) {
log.error("Dify 流式对话调用失败", e);
handler.onError(e);
}
}
/**
* 从 langchain4j 消息列表中提取最后一条用户消息文本
*/
private String extractUserQuery(List<ChatMessage> messages) {
for (int i = messages.size() - 1; i >= 0; i--) {
ChatMessage msg = messages.get(i);
if (msg instanceof UserMessage) {
return ((UserMessage) msg).singleText();
}
}
return "";
}
private String normalizeBaseUrl(String baseUrl) {
if (baseUrl == null || baseUrl.isBlank()) {
throw new IllegalArgumentException("Dify API 地址(apiHost)不能为空");
}
return baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
}
/**
* Dify 回调适配器
* 将 Dify ChatStreamCallback 事件转发给 langchain4j StreamingChatResponseHandler
*/
private class DifyChatStreamAdapter implements io.github.imfangs.dify.client.callback.ChatStreamCallback {
private final StreamingChatResponseHandler handler;
private final StringBuilder fullResponse = new StringBuilder();
DifyChatStreamAdapter(StreamingChatResponseHandler handler) {
this.handler = handler;
}
@Override
public void onMessage(MessageEvent event) {
String answer = event.getAnswer();
if (answer != null) {
fullResponse.append(answer);
handler.onPartialResponse(answer);
}
// 保存 Dify conversation_id 以维持多轮对话
if (event.getConversationId() != null && chatRequest.getSessionId() != null) {
conversationService.saveMapping(chatRequest.getSessionId(), event.getConversationId());
}
}
@Override
public void onMessageEnd(MessageEndEvent event) {
// 保存 conversation_id
if (event.getConversationId() != null && chatRequest.getSessionId() != null) {
conversationService.saveMapping(chatRequest.getSessionId(), event.getConversationId());
}
// 构建完整的 ChatResponse 交给上层处理
AiMessage aiMessage = new AiMessage(fullResponse.toString());
ChatResponse response = ChatResponse.builder()
.aiMessage(aiMessage)
.id(event.getMessageId())
.build();
handler.onCompleteResponse(response);
}
@Override
public void onError(ErrorEvent event) {
handler.onError(new RuntimeException(event.getMessage()));
}
@Override
public void onException(Throwable throwable) {
handler.onError(throwable);
}
}
}