feat: 添加项目配置和依赖更新
配置更新: 1. 前端配置 - 添加 hook-fetch 依赖用于 HTTP 请求 - 更新 vite.config.mts 配置 - 添加 .npmrc 配置文件 2. 后端配置 - 更新 application.yml 和 application-dev.yml 配置 - 更新 docker-compose.yml 配置 3. 代码优化 - OSS 客户端优化 - SSE 管理器优化 - 聊天服务和向量存储策略优化 4. 项目文档 - 添加 CLAUDE.md 项目指南 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -105,10 +105,17 @@ public class ChatServiceFacade implements IChatService {
|
||||
*/
|
||||
public SseEmitter sseChat(ChatRequest chatRequest) {
|
||||
|
||||
// 4. 具体的服务实现
|
||||
// 获取用户信息和已存在的 SSE 连接
|
||||
Long userId = LoginHelper.getUserId();
|
||||
String tokenValue = StpUtil.getTokenValue();
|
||||
SseEmitter emitter = sseEmitterManager.connect(userId, tokenValue);
|
||||
|
||||
// 获取已存在的 SSE 连接(前端已通过 GET /resource/sse 建立)
|
||||
// 不再调用 connect() 以避免关闭前端的连接
|
||||
SseEmitter emitter = sseEmitterManager.getEmitter(userId, tokenValue);
|
||||
if (emitter == null) {
|
||||
// 如果没有已存在的连接,则建立新连接(兼容未预先建立连接的情况)
|
||||
emitter = sseEmitterManager.connect(userId, tokenValue);
|
||||
}
|
||||
|
||||
// 1. 根据模型名称查询完整配置
|
||||
ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
|
||||
@@ -205,66 +212,81 @@ public class ChatServiceFacade implements IChatService {
|
||||
* @param chatModelVo 聊天模型配置
|
||||
*/
|
||||
private void handleThinkingMode(ChatRequest chatRequest, List<ChatMessage> contextMessages, ChatModelVo chatModelVo) {
|
||||
// 步骤1: 配置MCP传输层 - 连接到bing-cn-mcp服务器
|
||||
McpTransport transport = new StdioMcpTransport.Builder()
|
||||
.command(List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y", "bing-cn-mcp"))
|
||||
.logEvents(true)
|
||||
.build();
|
||||
try {
|
||||
// 步骤1: 配置MCP传输层 - 连接到bing-cn-mcp服务器
|
||||
// 根据操作系统选择正确的命令路径
|
||||
String osName = System.getProperty("os.name").toLowerCase();
|
||||
List<String> mcpCommand = osName.contains("win")
|
||||
? List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y", "bing-cn-mcp")
|
||||
: List.of("npx", "-y", "bing-cn-mcp");
|
||||
|
||||
McpClient mcpClient = new DefaultMcpClient.Builder()
|
||||
.transport(transport)
|
||||
.build();
|
||||
McpTransport transport = new StdioMcpTransport.Builder()
|
||||
.command(mcpCommand)
|
||||
.logEvents(true)
|
||||
.build();
|
||||
|
||||
ToolProvider toolProvider = McpToolProvider.builder()
|
||||
.mcpClients(List.of(mcpClient))
|
||||
.build();
|
||||
McpClient mcpClient = new DefaultMcpClient.Builder()
|
||||
.transport(transport)
|
||||
.build();
|
||||
|
||||
// 配置echarts MCP
|
||||
McpTransport transport1 = new StdioMcpTransport.Builder()
|
||||
.command(List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y", "mcp-echarts"))
|
||||
.logEvents(true)
|
||||
.build();
|
||||
ToolProvider toolProvider = McpToolProvider.builder()
|
||||
.mcpClients(List.of(mcpClient))
|
||||
.build();
|
||||
|
||||
McpClient mcpClient1 = new DefaultMcpClient.Builder()
|
||||
.transport(transport1)
|
||||
.build();
|
||||
// 配置echarts MCP
|
||||
List<String> echartsCommand = osName.contains("win")
|
||||
? List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y", "mcp-echarts")
|
||||
: List.of("npx", "-y", "mcp-echarts");
|
||||
|
||||
ToolProvider toolProvider1 = McpToolProvider.builder()
|
||||
.mcpClients(List.of(mcpClient1))
|
||||
.build();
|
||||
McpTransport transport1 = new StdioMcpTransport.Builder()
|
||||
.command(echartsCommand)
|
||||
.logEvents(true)
|
||||
.build();
|
||||
|
||||
// 配置模型
|
||||
OpenAiChatModel plannerModel = OpenAiChatModel.builder()
|
||||
.baseUrl(chatModelVo.getApiHost())
|
||||
.apiKey(chatModelVo.getApiKey())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.build();
|
||||
McpClient mcpClient1 = new DefaultMcpClient.Builder()
|
||||
.transport(transport1)
|
||||
.build();
|
||||
|
||||
// 构建各Agent
|
||||
SqlAgent sqlAgent = AgenticServices.agentBuilder(SqlAgent.class)
|
||||
.chatModel(plannerModel)
|
||||
.tools(new QueryAllTablesTool(), new QueryTableSchemaTool(), new ExecuteSqlQueryTool())
|
||||
.build();
|
||||
ToolProvider toolProvider1 = McpToolProvider.builder()
|
||||
.mcpClients(List.of(mcpClient1))
|
||||
.build();
|
||||
|
||||
WebSearchAgent searchAgent = AgenticServices.agentBuilder(WebSearchAgent.class)
|
||||
.chatModel(plannerModel)
|
||||
.toolProvider(toolProvider)
|
||||
.build();
|
||||
// 配置模型
|
||||
OpenAiChatModel plannerModel = OpenAiChatModel.builder()
|
||||
.baseUrl(chatModelVo.getApiHost())
|
||||
.apiKey(chatModelVo.getApiKey())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.build();
|
||||
|
||||
ChartGenerationAgent chartGenerationAgent = AgenticServices.agentBuilder(ChartGenerationAgent.class)
|
||||
.chatModel(plannerModel)
|
||||
.toolProvider(toolProvider1)
|
||||
.build();
|
||||
// 构建各Agent
|
||||
SqlAgent sqlAgent = AgenticServices.agentBuilder(SqlAgent.class)
|
||||
.chatModel(plannerModel)
|
||||
.tools(new QueryAllTablesTool(), new QueryTableSchemaTool(), new ExecuteSqlQueryTool())
|
||||
.build();
|
||||
|
||||
// 构建监督者Agent
|
||||
SupervisorAgent supervisor = AgenticServices.supervisorBuilder()
|
||||
.chatModel(plannerModel)
|
||||
.subAgents(sqlAgent, chartGenerationAgent)
|
||||
.responseStrategy(SupervisorResponseStrategy.LAST)
|
||||
.build();
|
||||
WebSearchAgent searchAgent = AgenticServices.agentBuilder(WebSearchAgent.class)
|
||||
.chatModel(plannerModel)
|
||||
.toolProvider(toolProvider)
|
||||
.build();
|
||||
|
||||
String invoke = supervisor.invoke(chatRequest.getContent());
|
||||
contextMessages.add(AiMessage.from(invoke));
|
||||
ChartGenerationAgent chartGenerationAgent = AgenticServices.agentBuilder(ChartGenerationAgent.class)
|
||||
.chatModel(plannerModel)
|
||||
.toolProvider(toolProvider1)
|
||||
.build();
|
||||
|
||||
// 构建监督者Agent
|
||||
SupervisorAgent supervisor = AgenticServices.supervisorBuilder()
|
||||
.chatModel(plannerModel)
|
||||
.subAgents(sqlAgent, chartGenerationAgent)
|
||||
.responseStrategy(SupervisorResponseStrategy.LAST)
|
||||
.build();
|
||||
|
||||
String invoke = supervisor.invoke(chatRequest.getContent());
|
||||
contextMessages.add(AiMessage.from(invoke));
|
||||
} catch (Exception e) {
|
||||
log.error("深度思考模式执行失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("深度思考模式执行失败,请检查 MCP 服务是否正确配置: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,8 @@ import org.springframework.stereotype.Service;
|
||||
import org.hzhub.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.hzhub.common.chat.domain.vo.chat.ChatModelVo;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* OllamaAI服务调用
|
||||
*
|
||||
@@ -24,6 +26,7 @@ public class OllamaServiceImpl implements AbstractChatService {
|
||||
return OllamaStreamingChatModel.builder()
|
||||
.baseUrl(chatModelVo.getApiHost())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.timeout(Duration.ofMinutes(3)) // 设置 3 分钟超时,适应本地模型较长的响应时间
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,11 @@ public class MilvusVectorStoreStrategy extends AbstractVectorStoreStrategy {
|
||||
*/
|
||||
private int getModelDimension(String modelName) {
|
||||
ChatModelVo modelConfig = chatModelService.selectModelByName(modelName);
|
||||
return modelConfig.getModelDimension();
|
||||
Integer dimension = modelConfig.getModelDimension();
|
||||
if (dimension == null) {
|
||||
throw new IllegalArgumentException("模型 " + modelName + " 的维度配置为空,请在数据库中配置正确的 model_dimension 值");
|
||||
}
|
||||
return dimension;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -100,25 +100,73 @@ public class WeaviateVectorStoreStrategy extends AbstractVectorStoreStrategy {
|
||||
String docId = storeEmbeddingBo.getDocId();
|
||||
log.info("向量存储条数记录: {}", chunkList.size());
|
||||
long startTime = System.currentTimeMillis();
|
||||
int successCount = 0;
|
||||
int skipCount = 0;
|
||||
|
||||
for (int i = 0; i < chunkList.size(); i++) {
|
||||
String text = chunkList.get(i);
|
||||
String fid = fidList.get(i);
|
||||
Embedding embedding = embeddingModel.embed(text).content();
|
||||
Map<String, Object> properties = Map.of(
|
||||
"text", text,
|
||||
"fid", fid,
|
||||
"kid", kid,
|
||||
"docId", docId
|
||||
);
|
||||
Float[] vector = toObjectArray(embedding.vector());
|
||||
client.data().creator()
|
||||
.withClassName("LocalKnowledge" + kid)
|
||||
.withProperties(properties)
|
||||
.withVector(vector)
|
||||
.run();
|
||||
|
||||
// 跳过空文本或仅包含空白字符的文本
|
||||
if (text == null || text.trim().isEmpty()) {
|
||||
log.warn("跳过空文本块,索引: {}, fid: {}", i, fid);
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 文本预处理:移除控制字符和特殊字符
|
||||
text = text.replaceAll("[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]", "")
|
||||
.replaceAll("\\s+", " ")
|
||||
.trim();
|
||||
|
||||
if (text.isEmpty()) {
|
||||
log.warn("文本预处理后为空,跳过,索引: {}, fid: {}", i, fid);
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
Embedding embedding = embeddingModel.embed(text).content();
|
||||
|
||||
// 检查 embedding 是否包含 NaN
|
||||
float[] vectorData = embedding.vector();
|
||||
boolean hasNaN = false;
|
||||
for (float v : vectorData) {
|
||||
if (Float.isNaN(v) || Float.isInfinite(v)) {
|
||||
hasNaN = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNaN) {
|
||||
log.error("Embedding 向量包含 NaN 或 Infinite 值,跳过该文本块,索引: {}, 文本长度: {}", i, text.length());
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
Map<String, Object> properties = Map.of(
|
||||
"text", text,
|
||||
"fid", fid,
|
||||
"kid", kid,
|
||||
"docId", docId
|
||||
);
|
||||
Float[] vector = toObjectArray(embedding.vector());
|
||||
client.data().creator()
|
||||
.withClassName("LocalKnowledge" + kid)
|
||||
.withProperties(properties)
|
||||
.withVector(vector)
|
||||
.run();
|
||||
successCount++;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理文本块失败,索引: {}, fid: {}, 错误: {}", i, fid, e.getMessage());
|
||||
skipCount++;
|
||||
// 继续处理下一个文本块,不中断整个流程
|
||||
}
|
||||
}
|
||||
|
||||
long endTime = System.currentTimeMillis();
|
||||
log.info("向量存储完成消耗时间:" + (endTime - startTime) / 1000 + "秒");
|
||||
log.info("向量存储完成,成功: {}, 跳过: {}, 消耗时间: {}秒", successCount, skipCount, (endTime - startTime) / 1000);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user