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

333
CLAUDE.md Normal file
View File

@@ -0,0 +1,333 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
HZHub (汇智中台) is an enterprise-level business platform built on HZHub-AI, integrating AI capabilities with ERP data adaptation. It consists of multiple frontend portals, a backend AI service, an ERP service, and a gateway, orchestrated via Docker Compose.
## Commands
### Docker Deployment (Production-like)
```bash
# Start all services (recommended for integration testing)
cd hzhub-deploy
docker-compose up -d
# View service status
docker-compose ps
# View logs
docker-compose logs -f hzhub-ai
docker-compose logs -f hzhub-admin
# Restart services
docker-compose restart hzhub-ai
# Stop all services
docker-compose down
```
### Backend Development (Spring Boot)
```bash
# Run AI service locally
cd hzhub-ai/hzhub-admin
mvn spring-boot:run -Dspring-boot.run.profiles=dev
# Build all modules
cd hzhub-ai
mvn clean package
# Build specific module
cd hzhub-ai/hzhub-modules/hzhub-chat
mvn clean package
# Run tests
mvn test
```
### Frontend Development (Vue 3 + Vben Admin)
```bash
# Admin portal development
cd hzhub-admin
pnpm install # Install dependencies
pnpm dev # Start dev server
pnpm build # Build all packages
pnpm --filter=@vben/web-antd build:prod # Build admin frontend
# Company portal development
cd hzhub-portal-company
pnpm install
pnpm dev
# Dealer portal development
cd hzhub-portal-dealer
pnpm install
pnpm dev
```
## Architecture
### Multi-Service Structure
```
┌─────────────────────────────────────────┐
│ Frontend Layer │
│ hzhub-admin | hzhub-portal-company │
│ | hzhub-portal-dealer │
└────────────┬────────────────────────────┘
┌────────┴────────┐
│ hzhub-gateway │ (API Gateway - planned)
│ Spring Cloud │ Auth, routing, rate limiting
└────────┬────────┘
┌────────┴────────┬────────────┐
│ hzhub-ai │ hzhub-erp │
│ (AI Service) │ (Planned) │
│ Spring Boot │ JDBC to │
│ 3.5.8 │ SQL Server│
└─────────────────┴────────────┘
```
### Backend Module Organization
**hzhub-ai** is organized as a multi-module Maven project:
- **hzhub-admin**: Main application entry point (`HZHubAIApplication.java`), configuration files
- **hzhub-common**: Shared utilities (core, redis, mybatis, security, satoken, oss, chat, etc.)
- **hzhub-modules**: Business modules
- **hzhub-chat**: Chat/AI conversation functionality
- **hzhub-system**: System management, users, roles, permissions
- **hzhub-workflow**: Workflow engine (Flowable-based)
- **hzhub-aiflow**: AI workflow orchestration
- **hzhub-generator**: Code generator
- **hzhub-extend**: Extensions (monitoring, job scheduling)
### Frontend Architecture
**hzhub-admin** uses a monorepo structure with pnpm + turbo:
```
hzhub-admin/
├── apps/
│ └── web-antd/ # Main admin application (Ant Design Vue)
│ ├── src/
│ │ ├── api/ # API calls
│ │ ├── views/ # Page components
│ │ ├── router/ # Vue Router config
│ │ └── store/ # Pinia stores
│ └── package.json
├── packages/ # Shared packages
└── package.json # Root monorepo config
```
**Portal applications** (hzhub-portal-company, hzhub-portal-dealer) are Vue 3 apps with:
- Composition API (`<script setup>`)
- Pinia state management with persistence
- Element Plus UI components
- hook-fetch for HTTP requests with SSE support
### Key Technologies
**Backend (hzhub-ai)**:
- Spring Boot 3.5.8, Spring AI 2.0
- LangChain4j for AI/LLM integration
- Sa-Token for authentication (JWT-based)
- MyBatis-Plus for database access
- Dynamic datasource for multi-database support
- Weaviate for vector database (knowledge retrieval)
**Frontend**:
- Vue 3 with Composition API
- Vben Admin framework (admin portal)
- Element Plus + element-plus-x (portal UI)
- Pinia for state management
- UnoCSS for atomic CSS
**Infrastructure**:
- MySQL 8.0 (business data)
- Redis 7 (cache, session)
- Weaviate 1.25.0 (vector database)
- n8n (workflow automation)
- MinIO (object storage)
## Important Patterns
### Backend API Structure
Controllers in `hzhub-modules/*/controller/` follow REST conventions:
```java
@RestController
@RequestMapping("/chat")
public class ChatController {
@PostMapping("/message")
public R<ChatResponse> sendMessage(@RequestBody ChatRequest request) {
// Response wrapper: R<T>
}
}
```
Response wrapper `R<T>` from `hzhub-common-core` provides standardized API responses.
### Multi-tenancy & Authentication
- Sa-Token handles JWT authentication
- Client ID header required: `ClientID` (passed in frontend requests)
- Token auto-injected via `Authorization: Bearer <token>`
- User permissions checked via Sa-Token's permission system
### Dynamic Datasource
`hzhub-common-mybatis` uses `dynamic-datasource-spring-boot-starter`:
```yaml
spring:
datasource:
dynamic:
primary: master
datasource:
master: # MySQL for business data
erp: # SQL Server (planned)
```
Switch datasource in code: `@Ds("erp")` annotation on service methods.
### SSE Streaming
AI chat uses Server-Sent Events for streaming responses:
```java
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(@RequestParam String message) {
// Returns SSE stream
}
```
Frontend uses `hook-fetch` with `sseTextDecoderPlugin` to handle SSE.
### Configuration Profiles
Backend uses Spring profiles:
- `application.yml` - Base configuration
- `application-dev.yml` - Development
- `application-prod.yml` - Production
Profile set via `SPRING_PROFILES_ACTIVE` env var or `spring.profiles.active` property.
## Development Workflow
### Service Dependencies
When running locally, backend services need infrastructure:
- MySQL (localhost:3306)
- Redis (localhost:6379)
- Weaviate (localhost:28080)
Start infrastructure via Docker Compose first:
```bash
cd hzhub-deploy
docker-compose up -d mysql redis weaviate
```
### Environment Variables
**Backend**:
- `SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_URL`: MySQL connection URL
- `SPING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_USERNAME`: DB username
- `SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_PASSWORD`: DB password
- `SPRING_DATA_REDIS_HOST`: Redis host
- `SPRING_DATA_REDIS_PORT`: Redis port
**Frontend** (in `.env.development`):
- `VITE_API_URL`: Backend API base URL (default: http://localhost:6039)
- `VITE_CLIENT_ID`: Client identifier for auth
- `VITE_WEB_TITLE`: Page title
### Testing
Backend tests use Spring Boot test framework:
```java
@SpringBootTest
class ChatServiceTest {
@Test
void testSendMessage() { ... }
}
```
Frontend uses Vitest for unit tests:
```bash
cd hzhub-admin
pnpm test:unit
```
## Deployment
### Docker Image Building
Backend Dockerfile in `hzhub-ai/Dockerfile`:
- Builds from Maven source
- Runs on port 6039
Frontend Dockerfiles in each portal directory:
- Builds with pnpm
- Served via Nginx
### Service Ports
| Service | Port | Access |
|---------|------|--------|
| hzhub-admin (frontend) | 5666 | http://localhost:5666 |
| hzhub-portal-company | 5137 | http://localhost:5137 |
| hzhub-portal-dealer | 5138 | http://localhost:5138 |
| hzhub-ai (backend API) | 6039 | http://localhost:6039 |
| MySQL | 3306 | localhost:3306 |
| Redis | 6379 | localhost:6379 |
| Weaviate | 28080 | http://localhost:28080 |
| n8n | 5678 | http://localhost:5678 |
## Common Tasks
### Adding a New Backend Module
1. Create module in `hzhub-ai/hzhub-modules/`
2. Add to parent `pom.xml` modules list
3. Include in `hzhub-admin` dependencies
4. Create controller, service, mapper following existing patterns
### Adding a New API Endpoint
1. Create controller in module's `controller/` package
2. Use `@RestController` and `@RequestMapping`
3. Return `R<T>` wrapper for responses
4. Add permission check if needed: `@SaCheckPermission("system:user:list")`
### Adding Frontend Features
For admin portal:
1. Add API call in `apps/web-antd/src/api/`
2. Create view in `apps/web-antd/src/views/`
3. Add route in `apps/web-antd/src/router/`
4. Add menu configuration
For portals (company/dealer):
1. Add API module in `src/api/` with `index.ts` and `types.ts`
2. Create page in `src/pages/`
3. Define route in `src/routers/modules/`
4. Add store if needed in `src/stores/`
### Working with Weaviate
Weaviate is used for knowledge retrieval (RAG). Collection creation and querying handled by LangChain4j integration in `hzhub-common-chat`.
## Notes
- **Package naming**: `org.hzhub.*` (renamed from `org.ruoyi.*`)
- **Java version**: JDK 17+
- **Node version**: Node 22+ with pnpm 10+
- **Code style**: ESLint `@antfu/eslint-config` for frontend; backend follows Spring conventions
- **Git hooks**: Lefthook configured (see `lefthook.yml`)

1
hzhub-admin/.npmrc Normal file
View File

@@ -0,0 +1 @@
auto-install-peers=true

View File

@@ -51,6 +51,7 @@
"cropperjs": "^1.6.2",
"dayjs": "catalog:",
"echarts": "^5.5.1",
"hook-fetch": "^2.3.2",
"lodash-es": "^4.17.21",
"pinia": "catalog:",
"tinymce": "7.9.1",

View File

@@ -72,6 +72,10 @@ export default defineConfig(async () => {
target: 'http://127.0.0.1:6039',
ws: true,
},
'/resource': {
changeOrigin: true,
target: 'http://127.0.0.1:6039',
},
},
},
},

View File

@@ -664,6 +664,9 @@ importers:
echarts:
specifier: ^5.5.1
version: 5.6.0
hook-fetch:
specifier: ^2.3.2
version: 2.3.2(react@19.2.4)(typescript-api-pro@1.0.1)(vue@3.5.25(typescript@5.9.3))
lodash-es:
specifier: ^4.17.21
version: 4.17.23
@@ -6897,6 +6900,7 @@ packages:
glob@10.5.0:
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
glob@11.1.0:
@@ -7038,6 +7042,13 @@ packages:
resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==}
engines: {node: '>=0.10.0'}
hook-fetch@2.3.2:
resolution: {integrity: sha512-56tODERta74J7O1TmSB/GvwQQlNaXIpzWLNXSnDfXxQDcofjwGlLIKeC27CzRHRb2ifb661wXA2H6D+MnvovxQ==}
peerDependencies:
react: '>=19.1.0'
typescript-api-pro: '>=0.0.6'
vue: ^3.5.17
hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
@@ -8991,6 +9002,10 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
radash@12.1.1:
resolution: {integrity: sha512-h36JMxKRqrAxVD8201FrCpyeNuUY9Y5zZwujr20fFO77tpUtGa6EZzfKw/3WaiBX95fq7+MpsuMLNdSnORAwSA==}
engines: {node: '>=14.18.0'}
radix-vue@1.9.17:
resolution: {integrity: sha512-mVCu7I2vXt1L2IUYHTt0sZMz7s1K2ZtqKeTIxG3yC5mMFfLBG4FtE1FDeRMpDd+Hhg/ybi9+iXmAP1ISREndoQ==}
peerDependencies:
@@ -9013,6 +9028,10 @@ packages:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
react@19.2.4:
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
engines: {node: '>=0.10.0'}
read-cache@1.0.0:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
@@ -9964,6 +9983,9 @@ packages:
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
engines: {node: '>= 0.4'}
typescript-api-pro@1.0.1:
resolution: {integrity: sha512-hu0JSP/Wfzmdf3BO/X2Cp/E+1FJKKWyoVsrmtG7s9SSH7CkHyKknNB7ZeaO7m14ALYCPxvEsa5LxQg/UcyvEig==}
typescript@5.8.2:
resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==}
engines: {node: '>=14.17'}
@@ -10480,6 +10502,7 @@ packages:
whatwg-encoding@3.1.1:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'}
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
whatwg-mimetype@3.0.0:
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
@@ -16787,6 +16810,14 @@ snapshots:
dependencies:
parse-passwd: 1.0.0
hook-fetch@2.3.2(react@19.2.4)(typescript-api-pro@1.0.1)(vue@3.5.25(typescript@5.9.3)):
dependencies:
qs: 6.14.0
radash: 12.1.1
react: 19.2.4
typescript-api-pro: 1.0.1
vue: 3.5.25(typescript@5.9.3)
hookable@5.5.3: {}
hookified@1.14.0: {}
@@ -18654,6 +18685,8 @@ snapshots:
queue-microtask@1.2.3: {}
radash@12.1.1: {}
radix-vue@1.9.17(vue@3.5.25(typescript@5.9.3)):
dependencies:
'@floating-ui/dom': 1.7.4
@@ -18691,6 +18724,8 @@ snapshots:
minimist: 1.2.8
strip-json-comments: 2.0.1
react@19.2.4: {}
read-cache@1.0.0:
dependencies:
pify: 2.3.0
@@ -19759,6 +19794,8 @@ snapshots:
possible-typed-array-names: 1.1.0
reflect.getprototypeof: 1.0.10
typescript-api-pro@1.0.1: {}
typescript@5.8.2: {}
typescript@5.9.3: {}

View File

@@ -58,7 +58,7 @@ spring:
driverClassName: com.mysql.cj.jdbc.Driver
# jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
url: jdbc:mysql://127.0.0.1:3306/hzhub_ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
url: jdbc:mysql://127.0.0.1:3306/ruoyi_ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
username: root
password: hzhub123
# agent:

View File

@@ -277,11 +277,11 @@ warm-flow:
vector-store:
# 向量存储类型 可选(weaviate/milvus)
# 如需修改向量库类型,请修改此配置值!
type: milvus
type: weaviate
# Weaviate配置
weaviate:
protocol: http
host: 127.0.0.1:6038
host: 127.0.0.1:28080
classname: LocalKnowledge
# Milvus配置
milvus:

View File

@@ -15,6 +15,7 @@ import org.hzhub.common.oss.properties.OssProperties;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.async.*;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3AsyncClient;
@@ -94,7 +95,14 @@ public class OssClient {
.region(of())
.forcePathStyle(isStyle)
.httpClient(NettyNioAsyncHttpClient.builder()
.connectionTimeout(Duration.ofSeconds(60)).build())
.connectionTimeout(Duration.ofMinutes(5))
.readTimeout(Duration.ofMinutes(5))
.writeTimeout(Duration.ofMinutes(5))
.build())
.overrideConfiguration(ClientOverrideConfiguration.builder()
.apiCallTimeout(Duration.ofMinutes(10))
.apiCallAttemptTimeout(Duration.ofMinutes(5))
.build())
.build();
//AWS基于 CRT 的 S3 AsyncClient 实例用作 S3 传输管理器的底层客户端
@@ -172,36 +180,22 @@ public class OssClient {
* @throws OssException 如果上传失败,抛出自定义异常
*/
public UploadResult upload(InputStream inputStream, String key, Long length, String contentType) {
// 如果输入流不是 ByteArrayInputStream则将其读取为字节数组再创建 ByteArrayInputStream
if (!(inputStream instanceof ByteArrayInputStream)) {
inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream));
}
// 输入流转换为字节数组,使用异步字节数组上传方式
try {
// 创建异步请求体length如果为空会报错
BlockingInputStreamAsyncRequestBody body = BlockingInputStreamAsyncRequestBody.builder()
.contentLength(length)
.subscribeTimeout(Duration.ofSeconds(120))
.build();
byte[] bytes = IoUtil.readBytes(inputStream);
// 使用 transferManager 进行上传
Upload upload = transferManager.upload(
x -> x.requestBody(body).addTransferListener(LoggingTransferListener.create())
.putObjectRequest(
y -> y.bucket(properties.getBucketName())
.key(key)
.contentType(contentType)
// 用于设置对象的访问控制列表ACL。不同云厂商对ACL的支持和实现方式有所不同
// 因此根据具体的云服务提供商你可能需要进行不同的配置自行开启阿里云有acl权限配置腾讯云没有acl权限配置
//.acl(getAccessPolicy().getObjectCannedACL())
.build())
.build());
// 使用异步方式上传字节数组
software.amazon.awssdk.services.s3.model.PutObjectResponse response = client.putObject(
software.amazon.awssdk.services.s3.model.PutObjectRequest.builder()
.bucket(properties.getBucketName())
.key(key)
.contentType(contentType)
.contentLength((long) bytes.length)
.build(),
software.amazon.awssdk.core.async.AsyncRequestBody.fromBytes(bytes)
).join();
// 将输入流写入请求体
body.writeInputStream(inputStream);
// 等待文件上传操作完成
CompletedUpload uploadResult = upload.completionFuture().join();
String eTag = uploadResult.response().eTag();
String eTag = response.eTag();
// 提取上传结果中的 ETag并构建一个自定义的 UploadResult 对象
return UploadResult.builder().url(getUrl() + StringUtils.SLASH + key).filename(key).eTag(eTag).build();

View File

@@ -93,6 +93,24 @@ public class SseEmitterManager {
return emitter;
}
/**
* 获取已存在的 SSE 连接(不创建新连接)
*
* @param userId 用户的唯一标识符
* @param token 用户的唯一令牌
* @return 返回已存在的 SseEmitter 实例,如果不存在则返回 null
*/
public SseEmitter getEmitter(Long userId, String token) {
if (userId == null || token == null) {
return null;
}
Map<String, SseEmitter> emitters = USER_TOKEN_EMITTERS.get(userId);
if (MapUtil.isNotEmpty(emitters)) {
return emitters.get(token);
}
return null;
}
/**
* 断开指定用户的 SSE 连接
*

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

View File

@@ -1,4 +1,3 @@
version: '3.8'
services:
# MySQL
@@ -54,6 +53,27 @@ services:
networks:
- hzhub-network
# MinIO (对象存储)
minio:
image: minio/minio:latest
container_name: hzhub-minio
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ROOT_USER: hzhub
MINIO_ROOT_PASSWORD: hzhub123456
command: server /data --console-address ":9001"
volumes:
- minio_data:/data
networks:
- hzhub-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
# hzhub-ai (AI服务)
hzhub-ai:
build:
@@ -141,6 +161,7 @@ volumes:
mysql_data:
redis_data:
weaviate_data:
minio_data:
n8n_data:
networks: