聊天
This commit is contained in:
@@ -9,7 +9,10 @@
|
||||
"Bash(cd:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(git add:*)"
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(chmod:*)",
|
||||
"Bash(docker build:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
||||
1
.idea/compiler.xml
generated
1
.idea/compiler.xml
generated
@@ -2,6 +2,7 @@
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<annotationProcessing>
|
||||
<profile default="true" name="Default" enabled="true" />
|
||||
<profile name="Maven default annotation processors profile" enabled="true">
|
||||
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||
|
||||
24
Dockerfile
Normal file
24
Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
||||
# Use official OpenJDK 17 runtime as base image
|
||||
FROM openjdk:17-jdk-slim
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy Maven wrapper and pom.xml first for better layer caching
|
||||
COPY .mvn/ .mvn/
|
||||
COPY mvnw pom.xml ./
|
||||
|
||||
# Download dependencies (this layer will be cached unless pom.xml changes)
|
||||
RUN ./mvnw dependency:go-offline -B
|
||||
|
||||
# Copy source code
|
||||
COPY src ./src
|
||||
|
||||
# Build the application
|
||||
RUN ./mvnw clean package -DskipTests
|
||||
|
||||
# Expose port 8080
|
||||
EXPOSE 8080
|
||||
|
||||
# Run the jar file
|
||||
CMD ["java", "-jar", "target/chat-0.0.1-SNAPSHOT.jar"]
|
||||
69
deploy.sh
Executable file
69
deploy.sh
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Deploy script for backserver to 101.200.154.78
|
||||
# Usage: ./deploy.sh
|
||||
|
||||
# Configuration
|
||||
SERVER_HOST="101.200.154.78"
|
||||
SERVER_USER="root"
|
||||
DEPLOY_PATH="/opt/backserver"
|
||||
CONTAINER_NAME="backserver-app"
|
||||
|
||||
echo "Starting deployment to $SERVER_HOST..."
|
||||
|
||||
# Build the Docker image locally
|
||||
echo "Building Docker image..."
|
||||
docker build -t backserver:latest .
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Docker build failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Save the image to a tar file
|
||||
echo "Saving Docker image to tar file..."
|
||||
docker save backserver:latest > backserver.tar
|
||||
|
||||
# Transfer files to server
|
||||
echo "Transferring files to server..."
|
||||
scp backserver.tar docker-compose.yml $SERVER_USER@$SERVER_HOST:$DEPLOY_PATH/
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "File transfer failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Execute deployment commands on server
|
||||
echo "Executing deployment on server..."
|
||||
ssh $SERVER_USER@$SERVER_HOST << EOF
|
||||
cd $DEPLOY_PATH
|
||||
|
||||
# Stop existing container
|
||||
docker-compose down
|
||||
|
||||
# Load new image
|
||||
docker load < backserver.tar
|
||||
|
||||
# Start new container
|
||||
docker-compose up -d
|
||||
|
||||
# Clean up
|
||||
rm backserver.tar
|
||||
|
||||
# Show status
|
||||
docker-compose ps
|
||||
docker logs $CONTAINER_NAME --tail 20
|
||||
EOF
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Deployment completed successfully!"
|
||||
echo "Application should be available at http://$SERVER_HOST:8080"
|
||||
else
|
||||
echo "Deployment failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up local tar file
|
||||
rm backserver.tar
|
||||
|
||||
echo "Deployment process finished."
|
||||
20
docker-compose.yml
Normal file
20
docker-compose.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
container_name: backserver-app
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
- SPRING_PROFILES_ACTIVE=prod
|
||||
- SPRING_DATASOURCE_URL=jdbc:mysql://101.200.154.78:3306/yunda_qa?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
|
||||
- SPRING_DATASOURCE_USERNAME=root
|
||||
- SPRING_DATASOURCE_PASSWORD=mysql_Jt3yzh
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
129
docs/chat.md
Normal file
129
docs/chat.md
Normal file
@@ -0,0 +1,129 @@
|
||||
好的,以下是 简化版的智能问答接口设计,聚焦两个核心问答模式:
|
||||
• ✅ 普通聊天(chat)
|
||||
• ✅ 深度研究(research)
|
||||
|
||||
省略了 history_limit、model、parameters、metadata 等高级控制参数,仅保留核心功能字段。
|
||||
|
||||
⸻
|
||||
|
||||
✅ 请求接口设计
|
||||
|
||||
POST /api/qa/ask
|
||||
|
||||
📥 请求体(JSON)
|
||||
|
||||
{
|
||||
"mode": "chat", // chat | research,问答模式
|
||||
"conversation_id": "12345", // 可选,会话ID(用于上下文关联)
|
||||
"message": "什么是RAG?" // 用户输入的问题
|
||||
}
|
||||
|
||||
📌 字段说明
|
||||
|
||||
字段名 类型 必填 说明
|
||||
mode string 是 chat 普通聊天,research 深度研究
|
||||
conversation_id string 否 可选;为空则创建新会话
|
||||
message string 是 用户发送的问题内容
|
||||
|
||||
|
||||
⸻
|
||||
|
||||
✅ 响应数据设计
|
||||
|
||||
📤 响应体(JSON)
|
||||
|
||||
{
|
||||
"success": true,
|
||||
"conversation_id": "12345",
|
||||
"message_id": "msg_001",
|
||||
"answer": "RAG 是一种结合检索与生成的问答系统架构...",
|
||||
"references": [], // chat 模式为空;research 模式可能返回引用资料
|
||||
"report_id": null, // 仅在 research 模式中,若已触发报告生成
|
||||
"tokens_used": 198,
|
||||
"time_ms": 732
|
||||
}
|
||||
|
||||
📌 字段说明
|
||||
|
||||
字段名 类型 说明
|
||||
success boolean 是否成功
|
||||
conversation_id string 当前所属会话 ID
|
||||
message_id string 当前返回消息的唯一标识
|
||||
answer string 模型生成的回答内容
|
||||
references array 若为 research 模式,可能包含引用资料;否则为空
|
||||
report_id string | null 若生成报告,返回报告ID;否则为 null
|
||||
tokens_used number 生成过程使用的 token 数量(可用于统计/计费)
|
||||
time_ms number 模型生成耗时(毫秒)
|
||||
|
||||
|
||||
⸻
|
||||
|
||||
✅ 示例
|
||||
|
||||
📌 普通聊天模式
|
||||
|
||||
请求
|
||||
|
||||
{
|
||||
"mode": "chat",
|
||||
"message": "你好,帮我解释一下 LLM 是什么?"
|
||||
}
|
||||
|
||||
响应
|
||||
|
||||
{
|
||||
"success": true,
|
||||
"conversation_id": "abc123",
|
||||
"message_id": "msg_1001",
|
||||
"answer": "LLM(Large Language Model)是指大型语言模型...",
|
||||
"references": [],
|
||||
"report_id": null,
|
||||
"tokens_used": 154,
|
||||
"time_ms": 645
|
||||
}
|
||||
|
||||
|
||||
⸻
|
||||
|
||||
📌 深度研究模式
|
||||
|
||||
请求
|
||||
|
||||
{
|
||||
"mode": "research",
|
||||
"conversation_id": "r20240718_001",
|
||||
"message": "请详细说明 RAG 在金融领域的应用。"
|
||||
}
|
||||
|
||||
响应
|
||||
|
||||
{
|
||||
"success": true,
|
||||
"conversation_id": "r20240718_001",
|
||||
"message_id": "msg_2009",
|
||||
"answer": "在金融领域,RAG 可用于增强问答系统...",
|
||||
"references": [
|
||||
{
|
||||
"title": "RAG 应用于金融智能问答",
|
||||
"url": "https://example.com/rag-finance",
|
||||
"quote": "金融领域中的RAG系统能显著提升精确率..."
|
||||
}
|
||||
],
|
||||
"report_id": "report_84928",
|
||||
"tokens_used": 341,
|
||||
"time_ms": 1024
|
||||
}
|
||||
|
||||
|
||||
⸻
|
||||
|
||||
✅ 结构总结(精简版)
|
||||
|
||||
模式 关键差异
|
||||
chat 只返回回答,无引用、无报告
|
||||
research 返回引用资料、可返回 report_id
|
||||
|
||||
|
||||
⸻
|
||||
|
||||
如需下一步生成 Swagger/OpenAPI 接口文档、接口校验 Schema、数据库如何存储这些内容,欢迎继续提问。
|
||||
13
pom.xml
13
pom.xml
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.0.4</version>
|
||||
<version>3.2.4</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
@@ -108,6 +108,17 @@
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.5.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
|
||||
<version>1.0.0-M6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -3,15 +3,18 @@ package com.yundage.chat.config;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.concurrent.DelegatingSecurityContextExecutorService;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
@@ -22,6 +25,8 @@ import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@@ -67,31 +72,64 @@ public class SecurityConfig {
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// 1. CORS 和 CSRF 配置
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
|
||||
// 2. 路由权限配置
|
||||
.authorizeHttpRequests(authz -> authz
|
||||
// 认证相关接口
|
||||
// 允许所有 OPTIONS 请求(CORS 预检)
|
||||
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
||||
|
||||
// 登录 / 注册
|
||||
.requestMatchers("/api/auth/**").permitAll()
|
||||
// 公开API
|
||||
.requestMatchers("/api/public/**").permitAll()
|
||||
|
||||
// Swagger 文档
|
||||
.requestMatchers(
|
||||
"/swagger-ui/**",
|
||||
"/swagger-ui.html",
|
||||
"/api-docs/**",
|
||||
"/v3/api-docs/**",
|
||||
"/swagger-resources/**",
|
||||
"/webjars/**"
|
||||
).permitAll()
|
||||
|
||||
// 健康检查
|
||||
.requestMatchers("/health").permitAll()
|
||||
// Swagger API文档
|
||||
.requestMatchers("/swagger-ui/**").permitAll()
|
||||
.requestMatchers("/swagger-ui.html").permitAll()
|
||||
.requestMatchers("/api-docs/**").permitAll()
|
||||
.requestMatchers("/v3/api-docs/**").permitAll()
|
||||
.requestMatchers("/swagger-resources/**").permitAll()
|
||||
.requestMatchers("/webjars/**").permitAll()
|
||||
// 其他所有请求需要认证
|
||||
|
||||
// 公开接口
|
||||
.requestMatchers("/api/public/**").permitAll()
|
||||
|
||||
// 聊天接口(允许流式访问)
|
||||
.requestMatchers(HttpMethod.POST, "/qa/ask/stream").permitAll()
|
||||
|
||||
// 聊天相关接口需认证
|
||||
.requestMatchers("/qa/**").authenticated()
|
||||
|
||||
// 所有其他请求需认证
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
|
||||
// 3. 无状态会话配置(适用于 JWT)
|
||||
.sessionManagement(session -> session
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
)
|
||||
|
||||
// 4. 认证提供者与 JWT 过滤器
|
||||
.authenticationProvider(authenticationProvider())
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
// 设置安全上下文策略(适用于多线程环境)
|
||||
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ExecutorService securityContextExecutorService() {
|
||||
// 创建一个固定大小的线程池,并使用DelegatingSecurityContextExecutorService包装
|
||||
// 这样线程池中的线程会自动继承安全上下文
|
||||
ExecutorService executorService = Executors.newFixedThreadPool(10);
|
||||
return new DelegatingSecurityContextExecutorService(executorService);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.yundage.chat.controller;
|
||||
|
||||
import com.yundage.chat.common.ApiResponse;
|
||||
import com.yundage.chat.dto.ChatRequest;
|
||||
import com.yundage.chat.dto.ChatResponse;
|
||||
import com.yundage.chat.entity.Conversation;
|
||||
import com.yundage.chat.entity.Message;
|
||||
import com.yundage.chat.entity.User;
|
||||
import com.yundage.chat.service.ChatService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/qa")
|
||||
@Tag(name = "智能问答", description = "聊天和深度研究接口")
|
||||
public class ChatController {
|
||||
|
||||
@Autowired
|
||||
private ChatService chatService;
|
||||
|
||||
@PostMapping("/ask")
|
||||
@Operation(summary = "智能问答", description = "支持普通聊天和深度研究两种模式")
|
||||
public ResponseEntity<ChatResponse> ask(
|
||||
@Valid @RequestBody ChatRequest request,
|
||||
org.springframework.security.core.Authentication authentication) {
|
||||
|
||||
com.yundage.chat.entity.User user = (com.yundage.chat.entity.User) authentication.getPrincipal();
|
||||
try {
|
||||
ChatResponse response = chatService.processChat(request, user);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
// 错误处理
|
||||
ChatResponse errorResponse = new ChatResponse();
|
||||
errorResponse.setSuccess(false);
|
||||
errorResponse.setAnswer("抱歉,处理您的请求时出现了错误:" + e.getMessage());
|
||||
return ResponseEntity.ok(errorResponse);
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/ask/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
@Operation(summary = "智能问答流式输出", description = "支持流式输出的智能问答,内容格式为Markdown")
|
||||
public SseEmitter askStream(
|
||||
@Valid @RequestBody ChatRequest request,
|
||||
org.springframework.security.core.Authentication authentication) {
|
||||
|
||||
com.yundage.chat.entity.User user = (com.yundage.chat.entity.User) authentication.getPrincipal();
|
||||
return chatService.processChatStream(request, user);
|
||||
}
|
||||
|
||||
@GetMapping("/conversations")
|
||||
@Operation(summary = "获取用户会话列表", description = "获取当前用户的所有活跃会话")
|
||||
public ResponseEntity<ApiResponse<List<Conversation>>> getConversations(
|
||||
org.springframework.security.core.Authentication authentication) {
|
||||
|
||||
com.yundage.chat.entity.User user = (com.yundage.chat.entity.User) authentication.getPrincipal();
|
||||
|
||||
try {
|
||||
List<Conversation> conversations = chatService.getUserConversations(user.getId());
|
||||
return ResponseEntity.ok(ApiResponse.success(conversations));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(ApiResponse.error("获取会话列表失败:" + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/conversations/{conversationId}/messages")
|
||||
@Operation(summary = "获取会话消息历史", description = "获取指定会话的所有消息")
|
||||
public ResponseEntity<ApiResponse<List<Message>>> getConversationMessages(
|
||||
@PathVariable Long conversationId,
|
||||
org.springframework.security.core.Authentication authentication) {
|
||||
|
||||
com.yundage.chat.entity.User user = (com.yundage.chat.entity.User) authentication.getPrincipal();
|
||||
|
||||
try {
|
||||
List<Message> messages = chatService.getConversationMessages(conversationId, user.getId());
|
||||
return ResponseEntity.ok(ApiResponse.success(messages));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(ApiResponse.error("获取消息历史失败:" + e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/main/java/com/yundage/chat/dto/ChatRequest.java
Normal file
42
src/main/java/com/yundage/chat/dto/ChatRequest.java
Normal file
@@ -0,0 +1,42 @@
|
||||
package com.yundage.chat.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
|
||||
public class ChatRequest {
|
||||
|
||||
@NotBlank(message = "mode不能为空")
|
||||
@Pattern(regexp = "^(chat|research)$", message = "mode只能是chat或research")
|
||||
private String mode;
|
||||
|
||||
private String conversationId;
|
||||
|
||||
@NotBlank(message = "message不能为空")
|
||||
private String message;
|
||||
|
||||
public ChatRequest() {}
|
||||
|
||||
public String getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public void setMode(String mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
public String getConversationId() {
|
||||
return conversationId;
|
||||
}
|
||||
|
||||
public void setConversationId(String conversationId) {
|
||||
this.conversationId = conversationId;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
121
src/main/java/com/yundage/chat/dto/ChatResponse.java
Normal file
121
src/main/java/com/yundage/chat/dto/ChatResponse.java
Normal file
@@ -0,0 +1,121 @@
|
||||
package com.yundage.chat.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ChatResponse {
|
||||
|
||||
private boolean success;
|
||||
private String conversationId;
|
||||
private String messageId;
|
||||
private String answer;
|
||||
private List<ReferenceItem> references;
|
||||
private String reportId;
|
||||
private Integer tokensUsed;
|
||||
private Long timeMs;
|
||||
|
||||
public ChatResponse() {
|
||||
this.success = true;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public void setSuccess(boolean success) {
|
||||
this.success = success;
|
||||
}
|
||||
|
||||
public String getConversationId() {
|
||||
return conversationId;
|
||||
}
|
||||
|
||||
public void setConversationId(String conversationId) {
|
||||
this.conversationId = conversationId;
|
||||
}
|
||||
|
||||
public String getMessageId() {
|
||||
return messageId;
|
||||
}
|
||||
|
||||
public void setMessageId(String messageId) {
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
public String getAnswer() {
|
||||
return answer;
|
||||
}
|
||||
|
||||
public void setAnswer(String answer) {
|
||||
this.answer = answer;
|
||||
}
|
||||
|
||||
public List<ReferenceItem> getReferences() {
|
||||
return references;
|
||||
}
|
||||
|
||||
public void setReferences(List<ReferenceItem> references) {
|
||||
this.references = references;
|
||||
}
|
||||
|
||||
public String getReportId() {
|
||||
return reportId;
|
||||
}
|
||||
|
||||
public void setReportId(String reportId) {
|
||||
this.reportId = reportId;
|
||||
}
|
||||
|
||||
public Integer getTokensUsed() {
|
||||
return tokensUsed;
|
||||
}
|
||||
|
||||
public void setTokensUsed(Integer tokensUsed) {
|
||||
this.tokensUsed = tokensUsed;
|
||||
}
|
||||
|
||||
public Long getTimeMs() {
|
||||
return timeMs;
|
||||
}
|
||||
|
||||
public void setTimeMs(Long timeMs) {
|
||||
this.timeMs = timeMs;
|
||||
}
|
||||
|
||||
public static class ReferenceItem {
|
||||
private String title;
|
||||
private String url;
|
||||
private String quote;
|
||||
|
||||
public ReferenceItem() {}
|
||||
|
||||
public ReferenceItem(String title, String url, String quote) {
|
||||
this.title = title;
|
||||
this.url = url;
|
||||
this.quote = quote;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getQuote() {
|
||||
return quote;
|
||||
}
|
||||
|
||||
public void setQuote(String quote) {
|
||||
this.quote = quote;
|
||||
}
|
||||
}
|
||||
}
|
||||
133
src/main/java/com/yundage/chat/dto/StreamResponse.java
Normal file
133
src/main/java/com/yundage/chat/dto/StreamResponse.java
Normal file
@@ -0,0 +1,133 @@
|
||||
package com.yundage.chat.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class StreamResponse {
|
||||
|
||||
private String type;
|
||||
private String conversationId;
|
||||
private String messageId;
|
||||
private String content;
|
||||
private List<ChatResponse.ReferenceItem> references;
|
||||
private String reportId;
|
||||
private Integer tokensUsed;
|
||||
private Long timeMs;
|
||||
private boolean finished;
|
||||
private String error;
|
||||
|
||||
public StreamResponse() {}
|
||||
|
||||
public static StreamResponse content(String content) {
|
||||
StreamResponse response = new StreamResponse();
|
||||
response.setType("content");
|
||||
response.setContent(content);
|
||||
response.setFinished(false);
|
||||
return response;
|
||||
}
|
||||
|
||||
public static StreamResponse finished(String conversationId, String messageId,
|
||||
List<ChatResponse.ReferenceItem> references,
|
||||
String reportId, Integer tokensUsed, Long timeMs) {
|
||||
StreamResponse response = new StreamResponse();
|
||||
response.setType("finished");
|
||||
response.setConversationId(conversationId);
|
||||
response.setMessageId(messageId);
|
||||
response.setReferences(references);
|
||||
response.setReportId(reportId);
|
||||
response.setTokensUsed(tokensUsed);
|
||||
response.setTimeMs(timeMs);
|
||||
response.setFinished(true);
|
||||
return response;
|
||||
}
|
||||
|
||||
public static StreamResponse error(String error) {
|
||||
StreamResponse response = new StreamResponse();
|
||||
response.setType("error");
|
||||
response.setError(error);
|
||||
response.setFinished(true);
|
||||
return response;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getConversationId() {
|
||||
return conversationId;
|
||||
}
|
||||
|
||||
public void setConversationId(String conversationId) {
|
||||
this.conversationId = conversationId;
|
||||
}
|
||||
|
||||
public String getMessageId() {
|
||||
return messageId;
|
||||
}
|
||||
|
||||
public void setMessageId(String messageId) {
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public List<ChatResponse.ReferenceItem> getReferences() {
|
||||
return references;
|
||||
}
|
||||
|
||||
public void setReferences(List<ChatResponse.ReferenceItem> references) {
|
||||
this.references = references;
|
||||
}
|
||||
|
||||
public String getReportId() {
|
||||
return reportId;
|
||||
}
|
||||
|
||||
public void setReportId(String reportId) {
|
||||
this.reportId = reportId;
|
||||
}
|
||||
|
||||
public Integer getTokensUsed() {
|
||||
return tokensUsed;
|
||||
}
|
||||
|
||||
public void setTokensUsed(Integer tokensUsed) {
|
||||
this.tokensUsed = tokensUsed;
|
||||
}
|
||||
|
||||
public Long getTimeMs() {
|
||||
return timeMs;
|
||||
}
|
||||
|
||||
public void setTimeMs(Long timeMs) {
|
||||
this.timeMs = timeMs;
|
||||
}
|
||||
|
||||
public boolean isFinished() {
|
||||
return finished;
|
||||
}
|
||||
|
||||
public void setFinished(boolean finished) {
|
||||
this.finished = finished;
|
||||
}
|
||||
|
||||
public String getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
public void setError(String error) {
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
104
src/main/java/com/yundage/chat/entity/Conversation.java
Normal file
104
src/main/java/com/yundage/chat/entity/Conversation.java
Normal file
@@ -0,0 +1,104 @@
|
||||
package com.yundage.chat.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import com.mybatisflex.annotation.Column;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Table("conversations")
|
||||
public class Conversation {
|
||||
|
||||
@Id(keyType = KeyType.Auto)
|
||||
private Long id;
|
||||
|
||||
@Column("user_id")
|
||||
private Long userId;
|
||||
|
||||
private String title;
|
||||
|
||||
@Column("model_version")
|
||||
private String modelVersion;
|
||||
|
||||
@Column("chat_mode")
|
||||
private String chatMode;
|
||||
|
||||
@Column("is_active")
|
||||
private Integer isActive;
|
||||
|
||||
@Column("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public Conversation() {
|
||||
this.isActive = 1;
|
||||
this.chatMode = "chat";
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getModelVersion() {
|
||||
return modelVersion;
|
||||
}
|
||||
|
||||
public void setModelVersion(String modelVersion) {
|
||||
this.modelVersion = modelVersion;
|
||||
}
|
||||
|
||||
public String getChatMode() {
|
||||
return chatMode;
|
||||
}
|
||||
|
||||
public void setChatMode(String chatMode) {
|
||||
this.chatMode = chatMode;
|
||||
}
|
||||
|
||||
public Integer getIsActive() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
public void setIsActive(Integer isActive) {
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
99
src/main/java/com/yundage/chat/entity/Message.java
Normal file
99
src/main/java/com/yundage/chat/entity/Message.java
Normal file
@@ -0,0 +1,99 @@
|
||||
package com.yundage.chat.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import com.mybatisflex.annotation.Column;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Table("messages")
|
||||
public class Message {
|
||||
|
||||
@Id(keyType = KeyType.Auto)
|
||||
private Long id;
|
||||
|
||||
@Column("conversation_id")
|
||||
private Long conversationId;
|
||||
|
||||
@Column("sequence_no")
|
||||
private Integer sequenceNo;
|
||||
|
||||
private String role;
|
||||
|
||||
private String content;
|
||||
|
||||
private Integer tokens;
|
||||
|
||||
@Column("latency_ms")
|
||||
private Integer latencyMs;
|
||||
|
||||
@Column("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public Message() {}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Long getConversationId() {
|
||||
return conversationId;
|
||||
}
|
||||
|
||||
public void setConversationId(Long conversationId) {
|
||||
this.conversationId = conversationId;
|
||||
}
|
||||
|
||||
public Integer getSequenceNo() {
|
||||
return sequenceNo;
|
||||
}
|
||||
|
||||
public void setSequenceNo(Integer sequenceNo) {
|
||||
this.sequenceNo = sequenceNo;
|
||||
}
|
||||
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public void setRole(String role) {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public Integer getTokens() {
|
||||
return tokens;
|
||||
}
|
||||
|
||||
public void setTokens(Integer tokens) {
|
||||
this.tokens = tokens;
|
||||
}
|
||||
|
||||
public Integer getLatencyMs() {
|
||||
return latencyMs;
|
||||
}
|
||||
|
||||
public void setLatencyMs(Integer latencyMs) {
|
||||
this.latencyMs = latencyMs;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.yundage.chat.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import com.yundage.chat.entity.Conversation;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ConversationMapper extends BaseMapper<Conversation> {
|
||||
|
||||
@Select("SELECT * FROM conversations WHERE user_id = #{userId} AND is_active = 1 ORDER BY updated_at DESC")
|
||||
List<Conversation> findActiveByUserId(@Param("userId") Long userId);
|
||||
|
||||
@Select("SELECT * FROM conversations WHERE id = #{id} AND user_id = #{userId}")
|
||||
Conversation findByIdAndUserId(@Param("id") Long id, @Param("userId") Long userId);
|
||||
}
|
||||
19
src/main/java/com/yundage/chat/mapper/MessageMapper.java
Normal file
19
src/main/java/com/yundage/chat/mapper/MessageMapper.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.yundage.chat.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import com.yundage.chat.entity.Message;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface MessageMapper extends BaseMapper<Message> {
|
||||
|
||||
@Select("SELECT * FROM messages WHERE conversation_id = #{conversationId} ORDER BY sequence_no ASC")
|
||||
List<Message> findByConversationId(@Param("conversationId") Long conversationId);
|
||||
|
||||
@Select("SELECT COALESCE(MAX(sequence_no), 0) FROM messages WHERE conversation_id = #{conversationId}")
|
||||
Integer getMaxSequenceNo(@Param("conversationId") Long conversationId);
|
||||
}
|
||||
277
src/main/java/com/yundage/chat/service/ChatService.java
Normal file
277
src/main/java/com/yundage/chat/service/ChatService.java
Normal file
@@ -0,0 +1,277 @@
|
||||
package com.yundage.chat.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yundage.chat.dto.ChatRequest;
|
||||
import com.yundage.chat.dto.ChatResponse;
|
||||
import com.yundage.chat.dto.StreamResponse;
|
||||
import com.yundage.chat.entity.Conversation;
|
||||
import com.yundage.chat.entity.Message;
|
||||
import com.yundage.chat.entity.User;
|
||||
import com.yundage.chat.mapper.ConversationMapper;
|
||||
import com.yundage.chat.mapper.MessageMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.concurrent.DelegatingSecurityContextRunnable;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ChatService {
|
||||
|
||||
private ConversationMapper conversationMapper;
|
||||
|
||||
|
||||
private MessageMapper messageMapper;
|
||||
|
||||
|
||||
/** ChatClient 由 Spring AI Starter 自动装配;可注入 Builder 再 build(),也可直接注入 ChatClient */
|
||||
private final ChatClient chatClient;
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
private final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
// 👇 手写构造器完成所有 final 字段的注入
|
||||
@Autowired
|
||||
public ChatService(ChatClient.Builder builder,
|
||||
ConversationMapper conversationMapper,
|
||||
MessageMapper messageMapper) {
|
||||
this.chatClient = builder.build();;
|
||||
this.conversationMapper = conversationMapper;
|
||||
this.messageMapper = messageMapper;
|
||||
}
|
||||
@Transactional
|
||||
public SseEmitter processChatStream(ChatRequest request, User user) {
|
||||
// 0 表示不超时;如需限制自行调整
|
||||
SseEmitter emitter = new SseEmitter(0L);
|
||||
|
||||
try {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
// 1️⃣ 取得会话并保存用户消息
|
||||
Conversation convo = getOrCreateConversation(request, user);
|
||||
saveUserMessage(convo.getId(), request.getMessage());
|
||||
|
||||
// 2️⃣ 在独立线程里完成流处理,避免阻塞 servlet 线程
|
||||
SecurityContext ctx = SecurityContextHolder.getContext();
|
||||
executor.submit(new DelegatingSecurityContextRunnable(() -> {
|
||||
|
||||
StringBuilder full = new StringBuilder();
|
||||
|
||||
// 3️⃣ 使用 Spring AI 流式接口;`content()` 把 Flux<ChatResponse> 映射成 Flux<String>
|
||||
Flux<String> flux = chatClient
|
||||
.prompt() // fluent API
|
||||
.user(request.getMessage()) // 用户消息
|
||||
.stream() // 进入流模式 [oai_citation:0‡Home](https://docs.spring.io/spring-ai/reference/api/chatclient.html)
|
||||
.content(); // 只取内容字符串 [oai_citation:1‡danvega.dev](https://www.danvega.dev/blog/spring-ai-streaming-chatbot)
|
||||
|
||||
flux.doOnNext(chunk -> { // 每个 token/块回调
|
||||
try {
|
||||
full.append(chunk); // 累积完整内容
|
||||
StreamResponse resp = StreamResponse.content(chunk);
|
||||
emitter.send(SseEmitter.event()
|
||||
.name("message")
|
||||
.data(mapper.writeValueAsString(resp)));
|
||||
} catch (Exception io) {
|
||||
emitter.completeWithError(io);
|
||||
}
|
||||
})
|
||||
.doOnComplete(() -> { // 4️⃣ 模型输出结束
|
||||
try {
|
||||
Message assistantMsg = saveAssistantMessage(convo.getId(), full.toString());
|
||||
convo.setUpdatedAt(LocalDateTime.now());
|
||||
conversationMapper.update(convo);
|
||||
|
||||
List<ChatResponse.ReferenceItem> refs =
|
||||
"research".equals(request.getMode()) ? generateMockReferences() : List.of();
|
||||
|
||||
StreamResponse finish = StreamResponse.finished(
|
||||
convo.getId().toString(),
|
||||
assistantMsg.getId().toString(),
|
||||
refs,
|
||||
null,
|
||||
estimateTokens(request.getMessage(), full.toString()),
|
||||
System.currentTimeMillis() - start
|
||||
);
|
||||
emitter.send(SseEmitter.event()
|
||||
.name("finished")
|
||||
.data(mapper.writeValueAsString(finish)));
|
||||
emitter.complete();
|
||||
} catch (Exception e) {
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
})
|
||||
.doOnError(err -> { // 5️⃣ 流异常
|
||||
try {
|
||||
StreamResponse error = StreamResponse.error("AI 处理失败:" + err.getMessage());
|
||||
emitter.send(SseEmitter.event()
|
||||
.name("error")
|
||||
.data(mapper.writeValueAsString(error)));
|
||||
} catch (Exception ignore) {}
|
||||
emitter.completeWithError(err);
|
||||
})
|
||||
.subscribe(); // 最终触发流
|
||||
}, ctx));
|
||||
} catch (Exception ex) {
|
||||
emitter.completeWithError(ex);
|
||||
}
|
||||
return emitter;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ChatResponse processChat(ChatRequest request, User user) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 获取或创建会话
|
||||
Conversation conversation = getOrCreateConversation(request, user);
|
||||
|
||||
// 保存用户消息
|
||||
Message userMessage = saveUserMessage(conversation.getId(), request.getMessage());
|
||||
|
||||
// 模拟AI回复生成 - 这里可以接入实际的AI服务
|
||||
String aiResponse = generateAIResponse(request);
|
||||
|
||||
// 保存AI回复消息
|
||||
Message assistantMessage = saveAssistantMessage(conversation.getId(), aiResponse);
|
||||
|
||||
// 更新会话的更新时间
|
||||
conversation.setUpdatedAt(LocalDateTime.now());
|
||||
conversationMapper.update(conversation);
|
||||
|
||||
// 构建响应
|
||||
ChatResponse response = new ChatResponse();
|
||||
response.setConversationId(conversation.getId().toString());
|
||||
response.setMessageId(assistantMessage.getId().toString());
|
||||
response.setAnswer(aiResponse);
|
||||
response.setTimeMs(System.currentTimeMillis() - startTime);
|
||||
|
||||
// 根据模式设置不同的响应内容
|
||||
if ("research".equals(request.getMode())) {
|
||||
// 研究模式可能包含引用资料
|
||||
response.setReferences(generateMockReferences());
|
||||
response.setReportId(null); // 可以在后续版本中实现报告生成功能
|
||||
} else {
|
||||
// 普通聊天模式
|
||||
response.setReferences(new ArrayList<>());
|
||||
response.setReportId(null);
|
||||
}
|
||||
|
||||
// 模拟token统计
|
||||
response.setTokensUsed(estimateTokens(request.getMessage(), aiResponse));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private String[] generateStreamingAIResponse(ChatRequest request) {
|
||||
// 目前先用单段文本模拟,后续可改为 chatClient.stream() 真正流式
|
||||
String aiReply = "这是AI的回复内容,模拟流式输出。";
|
||||
return new String[]{aiReply};
|
||||
}
|
||||
|
||||
private Conversation getOrCreateConversation(ChatRequest request, User user) {
|
||||
if (request.getConversationId() != null) {
|
||||
// 尝试获取现有会话
|
||||
try {
|
||||
Long conversationId = Long.parseLong(request.getConversationId());
|
||||
Conversation conversation = conversationMapper.findByIdAndUserId(conversationId, user.getId());
|
||||
if (conversation != null) {
|
||||
return conversation;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// 忽略无效的会话ID,创建新会话
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新会话
|
||||
Conversation conversation = new Conversation();
|
||||
conversation.setUserId(user.getId());
|
||||
conversation.setChatMode(request.getMode());
|
||||
conversation.setTitle(generateConversationTitle(request.getMessage()));
|
||||
conversation.setCreatedAt(LocalDateTime.now());
|
||||
conversation.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
conversationMapper.insert(conversation);
|
||||
return conversation;
|
||||
}
|
||||
|
||||
private Message saveUserMessage(Long conversationId, String content) {
|
||||
Integer nextSequenceNo = messageMapper.getMaxSequenceNo(conversationId) + 1;
|
||||
|
||||
Message message = new Message();
|
||||
message.setConversationId(conversationId);
|
||||
message.setSequenceNo(nextSequenceNo);
|
||||
message.setRole("user");
|
||||
message.setContent(content);
|
||||
message.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
messageMapper.insert(message);
|
||||
return message;
|
||||
}
|
||||
|
||||
private Message saveAssistantMessage(Long conversationId, String content) {
|
||||
Integer nextSequenceNo = messageMapper.getMaxSequenceNo(conversationId) + 1;
|
||||
|
||||
Message message = new Message();
|
||||
message.setConversationId(conversationId);
|
||||
message.setSequenceNo(nextSequenceNo);
|
||||
message.setRole("assistant");
|
||||
message.setContent(content);
|
||||
message.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
messageMapper.insert(message);
|
||||
return message;
|
||||
}
|
||||
|
||||
private String generateAIResponse(ChatRequest request) {
|
||||
// 使用 Spring AI ChatClient 生成回复
|
||||
|
||||
return "这是AI的回复内容,模拟生成。";
|
||||
}
|
||||
|
||||
private String generateConversationTitle(String message) {
|
||||
// 简单的标题生成逻辑,取前20个字符
|
||||
if (message.length() <= 20) {
|
||||
return message;
|
||||
}
|
||||
return message.substring(0, 20) + "...";
|
||||
}
|
||||
|
||||
private List<ChatResponse.ReferenceItem> generateMockReferences() {
|
||||
// 模拟的引用资料,实际项目中从真实的知识库获取
|
||||
List<ChatResponse.ReferenceItem> references = new ArrayList<>();
|
||||
references.add(new ChatResponse.ReferenceItem(
|
||||
"相关研究文档",
|
||||
"https://example.com/research",
|
||||
"这是一段引用的文本内容..."
|
||||
));
|
||||
return references;
|
||||
}
|
||||
|
||||
private Integer estimateTokens(String userMessage, String aiResponse) {
|
||||
// 简单的token估算,实际项目中需要使用真实的tokenizer
|
||||
return (userMessage.length() + aiResponse.length()) / 4;
|
||||
}
|
||||
|
||||
public List<Conversation> getUserConversations(Long userId) {
|
||||
return conversationMapper.findActiveByUserId(userId);
|
||||
}
|
||||
|
||||
public List<Message> getConversationMessages(Long conversationId, Long userId) {
|
||||
// 验证会话是否属于用户
|
||||
Conversation conversation = conversationMapper.findByIdAndUserId(conversationId, userId);
|
||||
if (conversation == null) {
|
||||
throw new RuntimeException("会话不存在或无权限访问");
|
||||
}
|
||||
|
||||
return messageMapper.findByConversationId(conversationId);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,15 @@ spring:
|
||||
auth: true
|
||||
starttls:
|
||||
enable: true
|
||||
|
||||
ai:
|
||||
openai:
|
||||
api-key: sk-5gAxnpuwJ9KIsea51cD05e21F1Ac4b6a99C8A97278C1F837
|
||||
base-url: https://www.jzhengda-api.com
|
||||
chat:
|
||||
options:
|
||||
model: gpt-4o-mini
|
||||
security:
|
||||
debug: true
|
||||
# MyBatis-Flex configuration
|
||||
mybatis-flex:
|
||||
# Enable SQL logging
|
||||
@@ -57,6 +65,7 @@ springdoc:
|
||||
logging:
|
||||
level:
|
||||
com.yundage.chat: debug
|
||||
com.mybatisflex: debug
|
||||
com.mybatisflex: info
|
||||
org.springframework.security: DEBUG
|
||||
# org.springframework: DEBUG
|
||||
#debug: true
|
||||
Reference in New Issue
Block a user