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:
大壮
2026-04-02 09:44:56 +00:00
parent ac8e6ca088
commit 2f25a943b8
14 changed files with 584 additions and 98 deletions

View File

@@ -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);
}
}
/**

View File

@@ -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();
}

View File

@@ -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

View File

@@ -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);
}